MP-2912 Margin trading implementation (#342)
* fix(useSWR): request hooks revalidation on focus disabled * [trade] add margin * [trade] include borrow rate in receipt * [trade] add tooltip margin and pr comments * updated regardign comments --------- Co-authored-by: Yusuf Seyrek <yusuf@delphilabs.io>
This commit is contained in:
parent
c25d8607e8
commit
b35c743286
@ -24,6 +24,7 @@
|
|||||||
"bignumber.js": "^9.1.1",
|
"bignumber.js": "^9.1.1",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"debounce-promise": "^3.1.2",
|
"debounce-promise": "^3.1.2",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"next": "13.4.9",
|
"next": "13.4.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -46,6 +47,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@types/debounce-promise": "^3.1.6",
|
"@types/debounce-promise": "^3.1.6",
|
||||||
|
"@types/lodash.throttle": "^4.1.7",
|
||||||
"@types/node": "^20.4.4",
|
"@types/node": "^20.4.4",
|
||||||
"@types/react": "18.2.14",
|
"@types/react": "18.2.14",
|
||||||
"@types/react-dom": "18.2.7",
|
"@types/react-dom": "18.2.7",
|
||||||
|
@ -6,12 +6,13 @@ interface Props {
|
|||||||
max: number
|
max: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const THUMB_WIDTH = 33
|
||||||
|
|
||||||
function InputOverlay({ max, value, marginThreshold }: Props) {
|
function InputOverlay({ max, value, marginThreshold }: Props) {
|
||||||
// 33 is the thumb width
|
const thumbPosPercent = max === 0 ? 0 : 100 / (max / value)
|
||||||
const thumbPosPercent = 100 / (max / value)
|
const thumbPadRight = (thumbPosPercent / 100) * THUMB_WIDTH
|
||||||
const thumbPadRight = (thumbPosPercent / 100) * 33
|
|
||||||
const markPosPercent = 100 / (max / (marginThreshold ?? 1))
|
const markPosPercent = 100 / (max / (marginThreshold ?? 1))
|
||||||
const markPadRight = (markPosPercent / 100) * 33
|
const markPadRight = (markPosPercent / 100) * THUMB_WIDTH
|
||||||
const hasPastMarginThreshold = marginThreshold ? value >= marginThreshold : undefined
|
const hasPastMarginThreshold = marginThreshold ? value >= marginThreshold : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -3,6 +3,8 @@ import classNames from 'classnames'
|
|||||||
|
|
||||||
import InputOverlay from 'components/RangeInput/InputOverlay'
|
import InputOverlay from 'components/RangeInput/InputOverlay'
|
||||||
|
|
||||||
|
const LEFT_MARGIN = 5
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
max: number
|
max: number
|
||||||
value: number
|
value: number
|
||||||
@ -23,6 +25,8 @@ function RangeInput(props: Props) {
|
|||||||
[onChange],
|
[onChange],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const markPosPercent = 100 / (max / (marginThreshold ?? 1))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(className.containerDefault, wrapperClassName, {
|
className={classNames(className.containerDefault, wrapperClassName, {
|
||||||
@ -46,7 +50,7 @@ function RangeInput(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={className.legendWrapper}>
|
<div className={className.legendWrapper}>
|
||||||
<span>0</span>
|
<span>{markPosPercent > LEFT_MARGIN ? 0 : ''}</span>
|
||||||
<span>{max.toFixed(2)}</span>
|
<span>{max.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -202,8 +202,8 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
|
|||||||
body: JSON.stringify({ query }),
|
body: JSON.stringify({ query }),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((json: { data: { candles: BarQueryData[] } }) => {
|
.then((json: { data?: { candles: BarQueryData[] } }) => {
|
||||||
return this.resolveBarData(json.data.candles, base)
|
return this.resolveBarData(json.data?.candles || [], base)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (this.debug) console.error(err)
|
if (this.debug) console.error(err)
|
||||||
|
@ -7,6 +7,7 @@ import ConditionalWrapper from 'hocs/ConditionalWrapper'
|
|||||||
interface Props {
|
interface Props {
|
||||||
checked: boolean
|
checked: boolean
|
||||||
onChange: (value: boolean) => void
|
onChange: (value: boolean) => void
|
||||||
|
borrowAssetSymbol: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,14 +19,24 @@ export default function MarginToggle(props: Props) {
|
|||||||
<ConditionalWrapper
|
<ConditionalWrapper
|
||||||
condition={!!props.disabled}
|
condition={!!props.disabled}
|
||||||
wrapper={(children) => (
|
wrapper={(children) => (
|
||||||
<Tooltip type='info' content={<Text size='sm'>Margin is not supported yet.</Text>}>
|
<Tooltip
|
||||||
|
type='info'
|
||||||
|
content={
|
||||||
|
<Text size='sm'>
|
||||||
|
{props.borrowAssetSymbol} is not a borrowable asset. Please choose another asset to
|
||||||
|
sell in order to margin trade.
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex flex-row'>
|
<div className='flex flex-row'>
|
||||||
<Switch {...props} name='margin' />
|
<Switch {...props} name='margin' />
|
||||||
<InfoCircle width={16} height={16} className='ml-2 mt-0.5 opacity-20' />
|
{props.disabled && (
|
||||||
|
<InfoCircle width={16} height={16} className='ml-2 mt-0.5 opacity-20' />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ConditionalWrapper>
|
</ConditionalWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useCallback, useMemo, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import ActionButton from 'components/Button/ActionButton'
|
import ActionButton from 'components/Button/ActionButton'
|
||||||
import useSwapRoute from 'hooks/useSwapRoute'
|
import useSwapRoute from 'hooks/useSwapRoute'
|
||||||
import { getAssetByDenom } from 'utils/assets'
|
import { getAssetByDenom } from 'utils/assets'
|
||||||
import { hardcodedFee } from 'utils/constants'
|
import { hardcodedFee } from 'utils/constants'
|
||||||
import { formatAmountWithSymbol } from 'utils/formatters'
|
import { formatAmountWithSymbol, formatPercent } from 'utils/formatters'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
buyAsset: Asset
|
buyAsset: Asset
|
||||||
sellAsset: Asset
|
sellAsset: Asset
|
||||||
|
borrowRate?: number | null
|
||||||
buyButtonDisabled: boolean
|
buyButtonDisabled: boolean
|
||||||
containerClassName?: string
|
containerClassName?: string
|
||||||
showProgressIndicator: boolean
|
showProgressIndicator: boolean
|
||||||
|
isMargin?: boolean
|
||||||
|
borrowAmount: BigNumber
|
||||||
buyAction: () => void
|
buyAction: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,9 +23,12 @@ export default function TradeSummary(props: Props) {
|
|||||||
const {
|
const {
|
||||||
buyAsset,
|
buyAsset,
|
||||||
sellAsset,
|
sellAsset,
|
||||||
|
borrowRate,
|
||||||
buyAction,
|
buyAction,
|
||||||
buyButtonDisabled,
|
buyButtonDisabled,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
|
isMargin,
|
||||||
|
borrowAmount,
|
||||||
showProgressIndicator,
|
showProgressIndicator,
|
||||||
} = props
|
} = props
|
||||||
const { data: routes, isLoading: isRouteLoading } = useSwapRoute(sellAsset.denom, buyAsset.denom)
|
const { data: routes, isLoading: isRouteLoading } = useSwapRoute(sellAsset.denom, buyAsset.denom)
|
||||||
@ -49,6 +55,24 @@ export default function TradeSummary(props: Props) {
|
|||||||
<span className={className.infoLineLabel}>Fees</span>
|
<span className={className.infoLineLabel}>Fees</span>
|
||||||
<span>{formatAmountWithSymbol(hardcodedFee.amount[0])}</span>
|
<span>{formatAmountWithSymbol(hardcodedFee.amount[0])}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{isMargin && (
|
||||||
|
<>
|
||||||
|
<div className={className.infoLine}>
|
||||||
|
<span className={className.infoLineLabel}>Borrowing</span>
|
||||||
|
<span>
|
||||||
|
{formatAmountWithSymbol({
|
||||||
|
denom: sellAsset.denom,
|
||||||
|
amount: borrowAmount.toString(),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={className.infoLine}>
|
||||||
|
<span className={className.infoLineLabel}>Borrow rate</span>
|
||||||
|
<span>{formatPercent(borrowRate || 0)}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={className.infoLine}>
|
<div className={className.infoLine}>
|
||||||
<span className={className.infoLineLabel}>Route</span>
|
<span className={className.infoLineLabel}>Route</span>
|
||||||
<span>{parsedRoutes}</span>
|
<span>{parsedRoutes}</span>
|
||||||
|
@ -8,10 +8,10 @@ import useCurrentAccount from 'hooks/useCurrentAccount'
|
|||||||
import useLocalStorage from 'hooks/useLocalStorage'
|
import useLocalStorage from 'hooks/useLocalStorage'
|
||||||
import usePrices from 'hooks/usePrices'
|
import usePrices from 'hooks/usePrices'
|
||||||
import useStore from 'store'
|
import useStore from 'store'
|
||||||
import { byDenom } from 'utils/array'
|
import { byDenom, byTokenDenom } from 'utils/array'
|
||||||
import { hardcodedFee } from 'utils/constants'
|
import { hardcodedFee } from 'utils/constants'
|
||||||
import RangeInput from 'components/RangeInput'
|
import RangeInput from 'components/RangeInput'
|
||||||
import { BN } from 'utils/helpers'
|
import { asyncThrottle, BN } from 'utils/helpers'
|
||||||
import AssetAmountInput from 'components/Trade/TradeModule/SwapForm/AssetAmountInput'
|
import AssetAmountInput from 'components/Trade/TradeModule/SwapForm/AssetAmountInput'
|
||||||
import MarginToggle from 'components/Trade/TradeModule/SwapForm/MarginToggle'
|
import MarginToggle from 'components/Trade/TradeModule/SwapForm/MarginToggle'
|
||||||
import OrderTypeSelector from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector'
|
import OrderTypeSelector from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector'
|
||||||
@ -20,6 +20,7 @@ import TradeSummary from 'components/Trade/TradeModule/SwapForm/TradeSummary'
|
|||||||
import { BNCoin } from 'types/classes/BNCoin'
|
import { BNCoin } from 'types/classes/BNCoin'
|
||||||
import estimateExactIn from 'api/swap/estimateExactIn'
|
import estimateExactIn from 'api/swap/estimateExactIn'
|
||||||
import useHealthComputer from 'hooks/useHealthComputer'
|
import useHealthComputer from 'hooks/useHealthComputer'
|
||||||
|
import useMarketBorrowings from 'hooks/useMarketBorrowings'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
buyAsset: Asset
|
buyAsset: Asset
|
||||||
@ -33,20 +34,77 @@ export default function SwapForm(props: Props) {
|
|||||||
const swap = useStore((s) => s.swap)
|
const swap = useStore((s) => s.swap)
|
||||||
const [slippage] = useLocalStorage(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)
|
const [slippage] = useLocalStorage(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)
|
||||||
const { computeMaxSwapAmount } = useHealthComputer(account)
|
const { computeMaxSwapAmount } = useHealthComputer(account)
|
||||||
const buyAmountEstimationTimeout = useRef<NodeJS.Timeout | undefined>(undefined)
|
const { data: borrowAssets } = useMarketBorrowings()
|
||||||
|
|
||||||
const [isMarginChecked, setMarginChecked] = useState(false)
|
const [isMarginChecked, setMarginChecked] = useState(false)
|
||||||
const [buyAssetAmount, setBuyAssetAmount] = useState(BN_ZERO)
|
const [buyAssetAmount, setBuyAssetAmount] = useState(BN_ZERO)
|
||||||
const [sellAssetAmount, setSellAssetAmount] = useState(BN_ZERO)
|
const [sellAssetAmount, setSellAssetAmount] = useState(BN_ZERO)
|
||||||
const focusedInput = useRef<'buy' | 'sell' | null>(null)
|
|
||||||
const [maxBuyableAmountEstimation, setMaxBuyableAmountEstimation] = useState(BN_ZERO)
|
const [maxBuyableAmountEstimation, setMaxBuyableAmountEstimation] = useState(BN_ZERO)
|
||||||
const [selectedOrderType, setSelectedOrderType] = useState<AvailableOrderType>('Market')
|
const [selectedOrderType, setSelectedOrderType] = useState<AvailableOrderType>('Market')
|
||||||
const [isTransactionExecuting, setTransactionExecuting] = useState(false)
|
const [isConfirming, setIsConfirming] = useState(false)
|
||||||
|
|
||||||
const [maxSellAssetAmount, maxSellAssetAmountStr] = useMemo(() => {
|
const throttledEstimateExactIn = useMemo(() => asyncThrottle(estimateExactIn, 250), [])
|
||||||
const amount = computeMaxSwapAmount(sellAsset.denom, buyAsset.denom, 'default')
|
|
||||||
return [amount, amount.toString()]
|
const borrowAsset = useMemo(
|
||||||
}, [computeMaxSwapAmount, sellAsset.denom, buyAsset.denom])
|
() => borrowAssets.find(byDenom(sellAsset.denom)),
|
||||||
|
[borrowAssets, sellAsset.denom],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onChangeSellAmount = useCallback(
|
||||||
|
(amount: BigNumber) => {
|
||||||
|
setSellAssetAmount(amount)
|
||||||
|
throttledEstimateExactIn(
|
||||||
|
{ denom: sellAsset.denom, amount: amount.toString() },
|
||||||
|
buyAsset.denom,
|
||||||
|
).then(setBuyAssetAmount)
|
||||||
|
},
|
||||||
|
[sellAsset.denom, throttledEstimateExactIn, buyAsset.denom],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onChangeBuyAmount = useCallback(
|
||||||
|
(amount: BigNumber) => {
|
||||||
|
setBuyAssetAmount(amount)
|
||||||
|
const swapFrom = {
|
||||||
|
denom: buyAsset.denom,
|
||||||
|
amount: amount.toString(),
|
||||||
|
}
|
||||||
|
throttledEstimateExactIn(swapFrom, sellAsset.denom).then(setSellAssetAmount)
|
||||||
|
},
|
||||||
|
[buyAsset.denom, throttledEstimateExactIn, sellAsset.denom],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRangeInputChange = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
onChangeSellAmount(BN(value).shiftedBy(sellAsset.decimals).integerValue())
|
||||||
|
},
|
||||||
|
[sellAsset.decimals, onChangeSellAmount],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [maxSellAmount, marginThreshold] = useMemo(() => {
|
||||||
|
const maxAmount = computeMaxSwapAmount(sellAsset.denom, buyAsset.denom, 'default')
|
||||||
|
const maxAmountOnMargin = computeMaxSwapAmount(sellAsset.denom, buyAsset.denom, 'margin')
|
||||||
|
|
||||||
|
estimateExactIn(
|
||||||
|
{
|
||||||
|
denom: sellAsset.denom,
|
||||||
|
amount: (isMarginChecked ? maxAmountOnMargin : maxAmount).toString(),
|
||||||
|
},
|
||||||
|
buyAsset.denom,
|
||||||
|
).then(setMaxBuyableAmountEstimation)
|
||||||
|
|
||||||
|
if (isMarginChecked) return [maxAmountOnMargin, maxAmount]
|
||||||
|
|
||||||
|
if (sellAssetAmount.isGreaterThan(maxAmount)) onChangeSellAmount(maxAmount)
|
||||||
|
|
||||||
|
return [maxAmount, maxAmount]
|
||||||
|
}, [
|
||||||
|
computeMaxSwapAmount,
|
||||||
|
sellAsset.denom,
|
||||||
|
buyAsset.denom,
|
||||||
|
isMarginChecked,
|
||||||
|
onChangeSellAmount,
|
||||||
|
sellAssetAmount,
|
||||||
|
])
|
||||||
|
|
||||||
const [buyAssetValue, sellAssetValue] = useMemo(() => {
|
const [buyAssetValue, sellAssetValue] = useMemo(() => {
|
||||||
const buyAssetPrice = prices.find(byDenom(buyAsset.denom))?.amount ?? BN_ZERO
|
const buyAssetPrice = prices.find(byDenom(buyAsset.denom))?.amount ?? BN_ZERO
|
||||||
@ -67,55 +125,22 @@ export default function SwapForm(props: Props) {
|
|||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
focusedInput.current = null
|
|
||||||
setBuyAssetAmount(BN_ZERO)
|
setBuyAssetAmount(BN_ZERO)
|
||||||
setSellAssetAmount(BN_ZERO)
|
setSellAssetAmount(BN_ZERO)
|
||||||
}, [buyAsset.denom, sellAsset.denom])
|
}, [buyAsset.denom, sellAsset.denom])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
estimateExactIn({ denom: sellAsset.denom, amount: maxSellAssetAmountStr }, buyAsset.denom).then(
|
|
||||||
setMaxBuyableAmountEstimation,
|
|
||||||
)
|
|
||||||
}, [maxSellAssetAmountStr, buyAsset.denom, sellAsset.denom])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (focusedInput.current === 'sell') {
|
|
||||||
if (buyAmountEstimationTimeout.current) {
|
|
||||||
clearTimeout(buyAmountEstimationTimeout.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
buyAmountEstimationTimeout.current = setTimeout(
|
|
||||||
() =>
|
|
||||||
estimateExactIn(
|
|
||||||
{ denom: sellAsset.denom, amount: sellAssetAmount.toString() },
|
|
||||||
buyAsset.denom,
|
|
||||||
)
|
|
||||||
.then(setBuyAssetAmount)
|
|
||||||
.then(() => clearTimeout(buyAmountEstimationTimeout?.current)),
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [buyAsset.denom, focusedInput, sellAsset.denom, sellAssetAmount])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (focusedInput.current === 'buy') {
|
|
||||||
estimateExactIn(
|
|
||||||
{
|
|
||||||
denom: buyAsset.denom,
|
|
||||||
amount: buyAssetAmount.toString(),
|
|
||||||
},
|
|
||||||
sellAsset.denom,
|
|
||||||
).then(setSellAssetAmount)
|
|
||||||
}
|
|
||||||
}, [buyAsset.denom, buyAssetAmount, focusedInput, sellAsset.denom])
|
|
||||||
|
|
||||||
const handleBuyClick = useCallback(async () => {
|
const handleBuyClick = useCallback(async () => {
|
||||||
if (account?.id) {
|
if (account?.id) {
|
||||||
setTransactionExecuting(true)
|
setIsConfirming(true)
|
||||||
|
const borrowCoin = sellAssetAmount.isGreaterThan(marginThreshold)
|
||||||
|
? BNCoin.fromDenomAndBigNumber(sellAsset.denom, sellAssetAmount.minus(marginThreshold))
|
||||||
|
: undefined
|
||||||
|
|
||||||
const isSucceeded = await swap({
|
const isSucceeded = await swap({
|
||||||
fee: hardcodedFee,
|
fee: hardcodedFee,
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
coinIn: BNCoin.fromDenomAndBigNumber(sellAsset.denom, sellAssetAmount.integerValue()),
|
coinIn: BNCoin.fromDenomAndBigNumber(sellAsset.denom, sellAssetAmount.integerValue()),
|
||||||
|
borrow: borrowCoin,
|
||||||
denomOut: buyAsset.denom,
|
denomOut: buyAsset.denom,
|
||||||
slippage,
|
slippage,
|
||||||
})
|
})
|
||||||
@ -123,72 +148,78 @@ export default function SwapForm(props: Props) {
|
|||||||
setSellAssetAmount(BN_ZERO)
|
setSellAssetAmount(BN_ZERO)
|
||||||
setBuyAssetAmount(BN_ZERO)
|
setBuyAssetAmount(BN_ZERO)
|
||||||
}
|
}
|
||||||
setTransactionExecuting(false)
|
setIsConfirming(false)
|
||||||
}
|
}
|
||||||
}, [account?.id, buyAsset.denom, sellAsset.denom, sellAssetAmount, slippage, swap])
|
}, [
|
||||||
|
account?.id,
|
||||||
const dismissInputFocus = useCallback(() => (focusedInput.current = null), [])
|
buyAsset.denom,
|
||||||
|
sellAsset.denom,
|
||||||
const handleRangeInputChange = useCallback(
|
sellAssetAmount,
|
||||||
(value: number) => {
|
slippage,
|
||||||
focusedInput.current = 'sell'
|
swap,
|
||||||
setSellAssetAmount(BN(value).shiftedBy(sellAsset.decimals).integerValue())
|
marginThreshold,
|
||||||
},
|
])
|
||||||
[sellAsset.decimals],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<MarginToggle checked={isMarginChecked} onChange={setMarginChecked} disabled />
|
<MarginToggle
|
||||||
|
checked={isMarginChecked}
|
||||||
|
onChange={setMarginChecked}
|
||||||
|
disabled={!borrowAsset?.isMarket}
|
||||||
|
borrowAssetSymbol={sellAsset.symbol}
|
||||||
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<OrderTypeSelector selected={selectedOrderType} onChange={setSelectedOrderType} />
|
<OrderTypeSelector selected={selectedOrderType} onChange={setSelectedOrderType} />
|
||||||
|
|
||||||
<AssetAmountInput
|
<AssetAmountInput
|
||||||
label='Buy'
|
label='Buy'
|
||||||
max={maxBuyableAmountEstimation}
|
max={maxBuyableAmountEstimation}
|
||||||
amount={buyAssetAmount}
|
amount={buyAssetAmount}
|
||||||
setAmount={setBuyAssetAmount}
|
setAmount={onChangeBuyAmount}
|
||||||
asset={buyAsset}
|
asset={buyAsset}
|
||||||
assetUSDValue={buyAssetValue}
|
assetUSDValue={buyAssetValue}
|
||||||
maxButtonLabel='Max Amount:'
|
maxButtonLabel='Max Amount:'
|
||||||
containerClassName='mx-3 my-6'
|
containerClassName='mx-3 my-6'
|
||||||
onBlur={dismissInputFocus}
|
disabled={isConfirming}
|
||||||
onFocus={() => (focusedInput.current = 'buy')}
|
|
||||||
disabled={isTransactionExecuting}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RangeInput
|
<RangeInput
|
||||||
wrapperClassName='p-4'
|
wrapperClassName='p-4'
|
||||||
onBlur={dismissInputFocus}
|
disabled={isConfirming || maxSellAmount.isZero()}
|
||||||
disabled={isTransactionExecuting || maxSellAssetAmount.isZero()}
|
|
||||||
onChange={handleRangeInputChange}
|
onChange={handleRangeInputChange}
|
||||||
value={sellAssetAmount.shiftedBy(-sellAsset.decimals).toNumber()}
|
value={sellAssetAmount.shiftedBy(-sellAsset.decimals).toNumber()}
|
||||||
max={maxSellAssetAmount.shiftedBy(-sellAsset.decimals).toNumber()}
|
max={maxSellAmount.shiftedBy(-sellAsset.decimals).toNumber()}
|
||||||
|
marginThreshold={
|
||||||
|
isMarginChecked ? marginThreshold.shiftedBy(-sellAsset.decimals).toNumber() : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AssetAmountInput
|
<AssetAmountInput
|
||||||
label='Sell'
|
label='Sell'
|
||||||
max={maxSellAssetAmount}
|
max={maxSellAmount}
|
||||||
amount={sellAssetAmount}
|
amount={sellAssetAmount}
|
||||||
setAmount={setSellAssetAmount}
|
setAmount={onChangeSellAmount}
|
||||||
assetUSDValue={sellAssetValue}
|
assetUSDValue={sellAssetValue}
|
||||||
asset={sellAsset}
|
asset={sellAsset}
|
||||||
maxButtonLabel='Balance:'
|
maxButtonLabel='Balance:'
|
||||||
containerClassName='mx-3'
|
containerClassName='mx-3'
|
||||||
onBlur={dismissInputFocus}
|
disabled={isConfirming}
|
||||||
onFocus={() => (focusedInput.current = 'sell')}
|
|
||||||
disabled={isTransactionExecuting}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TradeSummary
|
<TradeSummary
|
||||||
containerClassName='m-3 mt-10'
|
containerClassName='m-3 mt-10'
|
||||||
buyAsset={buyAsset}
|
buyAsset={buyAsset}
|
||||||
sellAsset={sellAsset}
|
sellAsset={sellAsset}
|
||||||
|
borrowRate={borrowAsset?.borrowRate}
|
||||||
buyAction={handleBuyClick}
|
buyAction={handleBuyClick}
|
||||||
buyButtonDisabled={sellAssetAmount.isZero()}
|
buyButtonDisabled={sellAssetAmount.isZero()}
|
||||||
showProgressIndicator={isTransactionExecuting}
|
showProgressIndicator={isConfirming}
|
||||||
|
isMargin={isMarginChecked}
|
||||||
|
borrowAmount={
|
||||||
|
sellAssetAmount.isGreaterThan(marginThreshold)
|
||||||
|
? sellAssetAmount.minus(marginThreshold)
|
||||||
|
: BN_ZERO
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||||
import { useShuttle } from '@delphi-labs/shuttle-react'
|
import { useShuttle } from '@delphi-labs/shuttle-react'
|
||||||
import { useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
|
|
||||||
import { CircularProgress } from 'components/CircularProgress'
|
import { CircularProgress } from 'components/CircularProgress'
|
||||||
import FullOverlayContent from 'components/FullOverlayContent'
|
import FullOverlayContent from 'components/FullOverlayContent'
|
||||||
@ -35,48 +35,64 @@ export default function WalletConnecting(props: Props) {
|
|||||||
const providerId = props.providerId ?? recentWallet?.providerId
|
const providerId = props.providerId ?? recentWallet?.providerId
|
||||||
const isAutoConnect = props.autoConnect
|
const isAutoConnect = props.autoConnect
|
||||||
|
|
||||||
const handleConnect = async (extensionProviderId: string) => {
|
const handleConnect = useCallback(
|
||||||
setIsConnecting(true)
|
(extensionProviderId: string) => {
|
||||||
|
async function handleConnectAsync() {
|
||||||
|
setIsConnecting(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const provider = extensionProviders.find((p) => p.id === providerId)
|
const provider = extensionProviders.find((p) => p.id === providerId)
|
||||||
const response =
|
const response =
|
||||||
isAutoConnect && provider
|
isAutoConnect && provider
|
||||||
? await provider.connect({ chainId: currentChainId })
|
? await provider.connect({ chainId: currentChainId })
|
||||||
: await connect({ extensionProviderId, chainId: currentChainId })
|
: await connect({ extensionProviderId, chainId: currentChainId })
|
||||||
const cosmClient = await CosmWasmClient.connect(response.network.rpc)
|
const cosmClient = await CosmWasmClient.connect(response.network.rpc)
|
||||||
const walletClient: WalletClient = {
|
const walletClient: WalletClient = {
|
||||||
broadcast,
|
broadcast,
|
||||||
cosmWasmClient: cosmClient,
|
cosmWasmClient: cosmClient,
|
||||||
connectedWallet: response,
|
connectedWallet: response,
|
||||||
sign,
|
sign,
|
||||||
simulate,
|
simulate,
|
||||||
|
}
|
||||||
|
setIsConnecting(false)
|
||||||
|
useStore.setState({
|
||||||
|
client: walletClient,
|
||||||
|
address: response.account.address,
|
||||||
|
focusComponent: <WalletFetchBalancesAndAccounts />,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setIsConnecting(false)
|
||||||
|
useStore.setState({
|
||||||
|
client: undefined,
|
||||||
|
address: undefined,
|
||||||
|
accounts: null,
|
||||||
|
focusComponent: (
|
||||||
|
<WalletSelect
|
||||||
|
error={{
|
||||||
|
title: 'Failed to connect to wallet',
|
||||||
|
message: mapErrorMessages(extensionProviderId, error.message),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setIsConnecting(false)
|
|
||||||
useStore.setState({
|
handleConnectAsync()
|
||||||
client: walletClient,
|
},
|
||||||
address: response.account.address,
|
[
|
||||||
focusComponent: <WalletFetchBalancesAndAccounts />,
|
broadcast,
|
||||||
})
|
connect,
|
||||||
} catch (error) {
|
extensionProviders,
|
||||||
if (error instanceof Error) {
|
isAutoConnect,
|
||||||
setIsConnecting(false)
|
providerId,
|
||||||
useStore.setState({
|
setIsConnecting,
|
||||||
client: undefined,
|
sign,
|
||||||
address: undefined,
|
simulate,
|
||||||
accounts: null,
|
],
|
||||||
focusComponent: (
|
)
|
||||||
<WalletSelect
|
|
||||||
error={{
|
|
||||||
title: 'Failed to connect to wallet',
|
|
||||||
message: mapErrorMessages(extensionProviderId, error.message),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnecting || !providerId) return
|
if (isConnecting || !providerId) return
|
||||||
|
@ -43,57 +43,66 @@ export default function useHealthComputer(account?: Account) {
|
|||||||
|
|
||||||
const vaultPositionValues = useMemo(() => {
|
const vaultPositionValues = useMemo(() => {
|
||||||
if (!account?.vaults) return null
|
if (!account?.vaults) return null
|
||||||
return account.vaults.reduce((prev, curr) => {
|
return account.vaults.reduce(
|
||||||
const baseCoinPrice = prices.find((price) => price.denom === curr.denoms.lp)?.amount || 0
|
(prev, curr) => {
|
||||||
prev[curr.address] = {
|
const baseCoinPrice = prices.find((price) => price.denom === curr.denoms.lp)?.amount || 0
|
||||||
base_coin: {
|
prev[curr.address] = {
|
||||||
amount: '0', // Not used by healthcomputer
|
base_coin: {
|
||||||
denom: curr.denoms.lp,
|
amount: '0', // Not used by healthcomputer
|
||||||
value: curr.amounts.unlocking.times(baseCoinPrice).integerValue().toString(),
|
denom: curr.denoms.lp,
|
||||||
},
|
value: curr.amounts.unlocking.times(baseCoinPrice).integerValue().toString(),
|
||||||
vault_coin: {
|
},
|
||||||
amount: '0', // Not used by healthcomputer
|
vault_coin: {
|
||||||
denom: curr.denoms.vault,
|
amount: '0', // Not used by healthcomputer
|
||||||
value: curr.values.primary
|
denom: curr.denoms.vault,
|
||||||
.div(baseCurrencyPrice)
|
value: curr.values.primary
|
||||||
.plus(curr.values.secondary.div(baseCurrencyPrice))
|
.div(baseCurrencyPrice)
|
||||||
.integerValue()
|
.plus(curr.values.secondary.div(baseCurrencyPrice))
|
||||||
.toString(),
|
.integerValue()
|
||||||
},
|
.toString(),
|
||||||
}
|
},
|
||||||
return prev
|
}
|
||||||
}, {} as { [key: string]: VaultPositionValue })
|
return prev
|
||||||
|
},
|
||||||
|
{} as { [key: string]: VaultPositionValue },
|
||||||
|
)
|
||||||
}, [account?.vaults, prices, baseCurrencyPrice])
|
}, [account?.vaults, prices, baseCurrencyPrice])
|
||||||
|
|
||||||
const priceData = useMemo(() => {
|
const priceData = useMemo(() => {
|
||||||
const baseCurrencyPrice =
|
const baseCurrencyPrice =
|
||||||
prices.find((price) => price.denom === baseCurrency.denom)?.amount || 0
|
prices.find((price) => price.denom === baseCurrency.denom)?.amount || 0
|
||||||
|
|
||||||
return prices.reduce((prev, curr) => {
|
return prices.reduce(
|
||||||
prev[curr.denom] = curr.amount.div(baseCurrencyPrice).decimalPlaces(18).toString()
|
(prev, curr) => {
|
||||||
return prev
|
prev[curr.denom] = curr.amount.div(baseCurrencyPrice).decimalPlaces(18).toString()
|
||||||
}, {} as { [key: string]: string })
|
return prev
|
||||||
|
},
|
||||||
|
{} as { [key: string]: string },
|
||||||
|
)
|
||||||
}, [prices, baseCurrency.denom])
|
}, [prices, baseCurrency.denom])
|
||||||
|
|
||||||
const denomsData = useMemo(
|
const denomsData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
assetParams.reduce((prev, curr) => {
|
assetParams.reduce(
|
||||||
const params: AssetParamsBaseForAddr = {
|
(prev, curr) => {
|
||||||
...curr,
|
const params: AssetParamsBaseForAddr = {
|
||||||
// The following overrides are required as testnet is 'broken' and new contracts are not updated yet
|
...curr,
|
||||||
// These overrides are not used by the healthcomputer internally, so they're not important anyways.
|
// The following overrides are required as testnet is 'broken' and new contracts are not updated yet
|
||||||
protocol_liquidation_fee: '1',
|
// These overrides are not used by the healthcomputer internally, so they're not important anyways.
|
||||||
liquidation_bonus: {
|
protocol_liquidation_fee: '1',
|
||||||
max_lb: '1',
|
liquidation_bonus: {
|
||||||
min_lb: '1',
|
max_lb: '1',
|
||||||
slope: '1',
|
min_lb: '1',
|
||||||
starting_lb: '1',
|
slope: '1',
|
||||||
},
|
starting_lb: '1',
|
||||||
}
|
},
|
||||||
prev[params.denom] = params
|
}
|
||||||
|
prev[params.denom] = params
|
||||||
|
|
||||||
return prev
|
return prev
|
||||||
}, {} as { [key: string]: AssetParamsBaseForAddr }),
|
},
|
||||||
|
{} as { [key: string]: AssetParamsBaseForAddr },
|
||||||
|
),
|
||||||
[assetParams],
|
[assetParams],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -103,10 +112,13 @@ export default function useHealthComputer(account?: Account) {
|
|||||||
const vaultPositionDenoms = positions.vaults.map((vault) => vault.vault.address)
|
const vaultPositionDenoms = positions.vaults.map((vault) => vault.vault.address)
|
||||||
return vaultConfigs
|
return vaultConfigs
|
||||||
.filter((config) => vaultPositionDenoms.includes(config.addr))
|
.filter((config) => vaultPositionDenoms.includes(config.addr))
|
||||||
.reduce((prev, curr) => {
|
.reduce(
|
||||||
prev[curr.addr] = curr
|
(prev, curr) => {
|
||||||
return prev
|
prev[curr.addr] = curr
|
||||||
}, {} as { [key: string]: VaultConfigBaseForString })
|
return prev
|
||||||
|
},
|
||||||
|
{} as { [key: string]: VaultConfigBaseForString },
|
||||||
|
)
|
||||||
}, [vaultConfigs, positions])
|
}, [vaultConfigs, positions])
|
||||||
|
|
||||||
const healthComputer: HealthComputer | null = useMemo(() => {
|
const healthComputer: HealthComputer | null = useMemo(() => {
|
||||||
|
@ -363,6 +363,7 @@ export default function createBroadcastSlice(
|
|||||||
fee: StdFee
|
fee: StdFee
|
||||||
accountId: string
|
accountId: string
|
||||||
coinIn: BNCoin
|
coinIn: BNCoin
|
||||||
|
borrow?: BNCoin
|
||||||
denomOut: string
|
denomOut: string
|
||||||
slippage: number
|
slippage: number
|
||||||
}) => {
|
}) => {
|
||||||
@ -370,6 +371,7 @@ export default function createBroadcastSlice(
|
|||||||
update_credit_account: {
|
update_credit_account: {
|
||||||
account_id: options.accountId,
|
account_id: options.accountId,
|
||||||
actions: [
|
actions: [
|
||||||
|
...(options.borrow ? [{ borrow: options.borrow.toCoin() }] : []),
|
||||||
{
|
{
|
||||||
swap_exact_in: {
|
swap_exact_in: {
|
||||||
coin_in: options.coinIn.toActionCoin(),
|
coin_in: options.coinIn.toActionCoin(),
|
||||||
|
1
src/types/interfaces/store/broadcast.d.ts
vendored
1
src/types/interfaces/store/broadcast.d.ts
vendored
@ -61,6 +61,7 @@ interface BroadcastSlice {
|
|||||||
fee: StdFee
|
fee: StdFee
|
||||||
accountId: string
|
accountId: string
|
||||||
coinIn: BNCoin
|
coinIn: BNCoin
|
||||||
|
borrow: BNCoin
|
||||||
denomOut: string
|
denomOut: string
|
||||||
slippage: number
|
slippage: number
|
||||||
}) => Promise<boolean>
|
}) => Promise<boolean>
|
||||||
|
@ -100,7 +100,7 @@ export function convertAccountToPositions(account: Account): Positions {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as VaultPosition),
|
}) as VaultPosition,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
|
import throttle from 'lodash.throttle'
|
||||||
|
|
||||||
BigNumber.config({ EXPONENTIAL_AT: 1e9 })
|
BigNumber.config({ EXPONENTIAL_AT: 1e9 })
|
||||||
export function BN(n: BigNumber.Value) {
|
export function BN(n: BigNumber.Value) {
|
||||||
@ -10,3 +11,15 @@ export function getApproximateHourlyInterest(amount: string, borrowRate: number)
|
|||||||
.dividedBy(24 * 365)
|
.dividedBy(24 * 365)
|
||||||
.multipliedBy(amount)
|
.multipliedBy(amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function asyncThrottle<F extends (...args: any[]) => Promise<any>>(func: F, wait?: number) {
|
||||||
|
const throttled = throttle((resolve, reject, args: Parameters<F>) => {
|
||||||
|
func(...args)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject)
|
||||||
|
}, wait)
|
||||||
|
return (...args: Parameters<F>): ReturnType<F> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
throttled(resolve, reject, args)
|
||||||
|
}) as ReturnType<F>
|
||||||
|
}
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -3564,6 +3564,13 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
|
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
|
||||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||||
|
|
||||||
|
"@types/lodash.throttle@^4.1.7":
|
||||||
|
version "4.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz#4ef379eb4f778068022310ef166625f420b6ba58"
|
||||||
|
integrity sha512-znwGDpjCHQ4FpLLx19w4OXDqq8+OvREa05H89obtSyXyOFKL3dDjCslsmfBz0T2FU8dmf5Wx1QvogbINiGIu9g==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
"@types/lodash.values@^4.3.6":
|
"@types/lodash.values@^4.3.6":
|
||||||
version "4.3.7"
|
version "4.3.7"
|
||||||
resolved "https://registry.npmjs.org/@types/lodash.values/-/lodash.values-4.3.7.tgz"
|
resolved "https://registry.npmjs.org/@types/lodash.values/-/lodash.values-4.3.7.tgz"
|
||||||
@ -7317,6 +7324,11 @@ lodash.sortby@^4.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
|
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
|
||||||
|
|
||||||
|
lodash.throttle@^4.1.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||||
|
integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
|
||||||
|
|
||||||
lodash.values@^4.3.0:
|
lodash.values@^4.3.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz"
|
resolved "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user