diff --git a/package.json b/package.json
index d196f896..4aa416d0 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"bignumber.js": "^9.1.1",
"classnames": "^2.3.2",
"debounce-promise": "^3.1.2",
+ "lodash.throttle": "^4.1.1",
"moment": "^2.29.4",
"next": "13.4.9",
"react": "^18.2.0",
@@ -46,6 +47,7 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"@types/debounce-promise": "^3.1.6",
+ "@types/lodash.throttle": "^4.1.7",
"@types/node": "^20.4.4",
"@types/react": "18.2.14",
"@types/react-dom": "18.2.7",
diff --git a/src/components/RangeInput/InputOverlay.tsx b/src/components/RangeInput/InputOverlay.tsx
index c99dc1d5..3af15fea 100644
--- a/src/components/RangeInput/InputOverlay.tsx
+++ b/src/components/RangeInput/InputOverlay.tsx
@@ -6,12 +6,13 @@ interface Props {
max: number
}
+const THUMB_WIDTH = 33
+
function InputOverlay({ max, value, marginThreshold }: Props) {
- // 33 is the thumb width
- const thumbPosPercent = 100 / (max / value)
- const thumbPadRight = (thumbPosPercent / 100) * 33
+ const thumbPosPercent = max === 0 ? 0 : 100 / (max / value)
+ const thumbPadRight = (thumbPosPercent / 100) * THUMB_WIDTH
const markPosPercent = 100 / (max / (marginThreshold ?? 1))
- const markPadRight = (markPosPercent / 100) * 33
+ const markPadRight = (markPosPercent / 100) * THUMB_WIDTH
const hasPastMarginThreshold = marginThreshold ? value >= marginThreshold : undefined
return (
diff --git a/src/components/RangeInput/index.tsx b/src/components/RangeInput/index.tsx
index 410dd87f..365db867 100644
--- a/src/components/RangeInput/index.tsx
+++ b/src/components/RangeInput/index.tsx
@@ -3,6 +3,8 @@ import classNames from 'classnames'
import InputOverlay from 'components/RangeInput/InputOverlay'
+const LEFT_MARGIN = 5
+
type Props = {
max: number
value: number
@@ -23,6 +25,8 @@ function RangeInput(props: Props) {
[onChange],
)
+ const markPosPercent = 100 / (max / (marginThreshold ?? 1))
+
return (
- 0
+ {markPosPercent > LEFT_MARGIN ? 0 : ''}
{max.toFixed(2)}
diff --git a/src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts b/src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts
index f83c701b..f04b6f57 100644
--- a/src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts
+++ b/src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts
@@ -202,8 +202,8 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
body: JSON.stringify({ query }),
})
.then((res) => res.json())
- .then((json: { data: { candles: BarQueryData[] } }) => {
- return this.resolveBarData(json.data.candles, base)
+ .then((json: { data?: { candles: BarQueryData[] } }) => {
+ return this.resolveBarData(json.data?.candles || [], base)
})
.catch((err) => {
if (this.debug) console.error(err)
diff --git a/src/components/Trade/TradeModule/SwapForm/MarginToggle.tsx b/src/components/Trade/TradeModule/SwapForm/MarginToggle.tsx
index c80c0d49..0fd58301 100644
--- a/src/components/Trade/TradeModule/SwapForm/MarginToggle.tsx
+++ b/src/components/Trade/TradeModule/SwapForm/MarginToggle.tsx
@@ -7,6 +7,7 @@ import ConditionalWrapper from 'hocs/ConditionalWrapper'
interface Props {
checked: boolean
onChange: (value: boolean) => void
+ borrowAssetSymbol: string
disabled?: boolean
}
@@ -18,14 +19,24 @@ export default function MarginToggle(props: Props) {
(
- Margin is not supported yet.}>
+
+ {props.borrowAssetSymbol} is not a borrowable asset. Please choose another asset to
+ sell in order to margin trade.
+
+ }
+ >
{children}
)}
>
-
+ {props.disabled && (
+
+ )}
diff --git a/src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx b/src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx
index ea8f8062..1431580e 100644
--- a/src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx
+++ b/src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx
@@ -1,18 +1,21 @@
import classNames from 'classnames'
-import { useCallback, useMemo, useState } from 'react'
+import { useMemo } from 'react'
import ActionButton from 'components/Button/ActionButton'
import useSwapRoute from 'hooks/useSwapRoute'
import { getAssetByDenom } from 'utils/assets'
import { hardcodedFee } from 'utils/constants'
-import { formatAmountWithSymbol } from 'utils/formatters'
+import { formatAmountWithSymbol, formatPercent } from 'utils/formatters'
interface Props {
buyAsset: Asset
sellAsset: Asset
+ borrowRate?: number | null
buyButtonDisabled: boolean
containerClassName?: string
showProgressIndicator: boolean
+ isMargin?: boolean
+ borrowAmount: BigNumber
buyAction: () => void
}
@@ -20,9 +23,12 @@ export default function TradeSummary(props: Props) {
const {
buyAsset,
sellAsset,
+ borrowRate,
buyAction,
buyButtonDisabled,
containerClassName,
+ isMargin,
+ borrowAmount,
showProgressIndicator,
} = props
const { data: routes, isLoading: isRouteLoading } = useSwapRoute(sellAsset.denom, buyAsset.denom)
@@ -49,6 +55,24 @@ export default function TradeSummary(props: Props) {
Fees
{formatAmountWithSymbol(hardcodedFee.amount[0])}
+ {isMargin && (
+ <>
+
+ Borrowing
+
+ {formatAmountWithSymbol({
+ denom: sellAsset.denom,
+ amount: borrowAmount.toString(),
+ })}
+
+
+
+ Borrow rate
+ {formatPercent(borrowRate || 0)}
+
+ >
+ )}
+
Route
{parsedRoutes}
diff --git a/src/components/Trade/TradeModule/SwapForm/index.tsx b/src/components/Trade/TradeModule/SwapForm/index.tsx
index 7df96b7e..6caa40ea 100644
--- a/src/components/Trade/TradeModule/SwapForm/index.tsx
+++ b/src/components/Trade/TradeModule/SwapForm/index.tsx
@@ -8,10 +8,10 @@ import useCurrentAccount from 'hooks/useCurrentAccount'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
-import { byDenom } from 'utils/array'
+import { byDenom, byTokenDenom } from 'utils/array'
import { hardcodedFee } from 'utils/constants'
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 MarginToggle from 'components/Trade/TradeModule/SwapForm/MarginToggle'
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 estimateExactIn from 'api/swap/estimateExactIn'
import useHealthComputer from 'hooks/useHealthComputer'
+import useMarketBorrowings from 'hooks/useMarketBorrowings'
interface Props {
buyAsset: Asset
@@ -33,20 +34,77 @@ export default function SwapForm(props: Props) {
const swap = useStore((s) => s.swap)
const [slippage] = useLocalStorage(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)
const { computeMaxSwapAmount } = useHealthComputer(account)
- const buyAmountEstimationTimeout = useRef
(undefined)
+ const { data: borrowAssets } = useMarketBorrowings()
const [isMarginChecked, setMarginChecked] = useState(false)
const [buyAssetAmount, setBuyAssetAmount] = useState(BN_ZERO)
const [sellAssetAmount, setSellAssetAmount] = useState(BN_ZERO)
- const focusedInput = useRef<'buy' | 'sell' | null>(null)
const [maxBuyableAmountEstimation, setMaxBuyableAmountEstimation] = useState(BN_ZERO)
const [selectedOrderType, setSelectedOrderType] = useState('Market')
- const [isTransactionExecuting, setTransactionExecuting] = useState(false)
+ const [isConfirming, setIsConfirming] = useState(false)
- const [maxSellAssetAmount, maxSellAssetAmountStr] = useMemo(() => {
- const amount = computeMaxSwapAmount(sellAsset.denom, buyAsset.denom, 'default')
- return [amount, amount.toString()]
- }, [computeMaxSwapAmount, sellAsset.denom, buyAsset.denom])
+ const throttledEstimateExactIn = useMemo(() => asyncThrottle(estimateExactIn, 250), [])
+
+ const borrowAsset = useMemo(
+ () => 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 buyAssetPrice = prices.find(byDenom(buyAsset.denom))?.amount ?? BN_ZERO
@@ -67,55 +125,22 @@ export default function SwapForm(props: Props) {
])
useEffect(() => {
- focusedInput.current = null
setBuyAssetAmount(BN_ZERO)
setSellAssetAmount(BN_ZERO)
}, [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 () => {
if (account?.id) {
- setTransactionExecuting(true)
+ setIsConfirming(true)
+ const borrowCoin = sellAssetAmount.isGreaterThan(marginThreshold)
+ ? BNCoin.fromDenomAndBigNumber(sellAsset.denom, sellAssetAmount.minus(marginThreshold))
+ : undefined
+
const isSucceeded = await swap({
fee: hardcodedFee,
accountId: account.id,
coinIn: BNCoin.fromDenomAndBigNumber(sellAsset.denom, sellAssetAmount.integerValue()),
+ borrow: borrowCoin,
denomOut: buyAsset.denom,
slippage,
})
@@ -123,72 +148,78 @@ export default function SwapForm(props: Props) {
setSellAssetAmount(BN_ZERO)
setBuyAssetAmount(BN_ZERO)
}
- setTransactionExecuting(false)
+ setIsConfirming(false)
}
- }, [account?.id, buyAsset.denom, sellAsset.denom, sellAssetAmount, slippage, swap])
-
- const dismissInputFocus = useCallback(() => (focusedInput.current = null), [])
-
- const handleRangeInputChange = useCallback(
- (value: number) => {
- focusedInput.current = 'sell'
- setSellAssetAmount(BN(value).shiftedBy(sellAsset.decimals).integerValue())
- },
- [sellAsset.decimals],
- )
+ }, [
+ account?.id,
+ buyAsset.denom,
+ sellAsset.denom,
+ sellAssetAmount,
+ slippage,
+ swap,
+ marginThreshold,
+ ])
return (
<>
-
-
+
-
(focusedInput.current = 'buy')}
- disabled={isTransactionExecuting}
+ disabled={isConfirming}
/>
(focusedInput.current = 'sell')}
- disabled={isTransactionExecuting}
+ disabled={isConfirming}
/>
>
)
diff --git a/src/components/Wallet/WalletConnecting.tsx b/src/components/Wallet/WalletConnecting.tsx
index f16832ed..c39a3dd4 100644
--- a/src/components/Wallet/WalletConnecting.tsx
+++ b/src/components/Wallet/WalletConnecting.tsx
@@ -1,6 +1,6 @@
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { useShuttle } from '@delphi-labs/shuttle-react'
-import { useEffect } from 'react'
+import { useCallback, useEffect } from 'react'
import { CircularProgress } from 'components/CircularProgress'
import FullOverlayContent from 'components/FullOverlayContent'
@@ -35,48 +35,64 @@ export default function WalletConnecting(props: Props) {
const providerId = props.providerId ?? recentWallet?.providerId
const isAutoConnect = props.autoConnect
- const handleConnect = async (extensionProviderId: string) => {
- setIsConnecting(true)
+ const handleConnect = useCallback(
+ (extensionProviderId: string) => {
+ async function handleConnectAsync() {
+ setIsConnecting(true)
- try {
- const provider = extensionProviders.find((p) => p.id === providerId)
- const response =
- isAutoConnect && provider
- ? await provider.connect({ chainId: currentChainId })
- : await connect({ extensionProviderId, chainId: currentChainId })
- const cosmClient = await CosmWasmClient.connect(response.network.rpc)
- const walletClient: WalletClient = {
- broadcast,
- cosmWasmClient: cosmClient,
- connectedWallet: response,
- sign,
- simulate,
+ try {
+ const provider = extensionProviders.find((p) => p.id === providerId)
+ const response =
+ isAutoConnect && provider
+ ? await provider.connect({ chainId: currentChainId })
+ : await connect({ extensionProviderId, chainId: currentChainId })
+ const cosmClient = await CosmWasmClient.connect(response.network.rpc)
+ const walletClient: WalletClient = {
+ broadcast,
+ cosmWasmClient: cosmClient,
+ connectedWallet: response,
+ sign,
+ simulate,
+ }
+ setIsConnecting(false)
+ useStore.setState({
+ client: walletClient,
+ address: response.account.address,
+ focusComponent: ,
+ })
+ } catch (error) {
+ if (error instanceof Error) {
+ setIsConnecting(false)
+ useStore.setState({
+ client: undefined,
+ address: undefined,
+ accounts: null,
+ focusComponent: (
+
+ ),
+ })
+ }
+ }
}
- setIsConnecting(false)
- useStore.setState({
- client: walletClient,
- address: response.account.address,
- focusComponent: ,
- })
- } catch (error) {
- if (error instanceof Error) {
- setIsConnecting(false)
- useStore.setState({
- client: undefined,
- address: undefined,
- accounts: null,
- focusComponent: (
-
- ),
- })
- }
- }
- }
+
+ handleConnectAsync()
+ },
+ [
+ broadcast,
+ connect,
+ extensionProviders,
+ isAutoConnect,
+ providerId,
+ setIsConnecting,
+ sign,
+ simulate,
+ ],
+ )
useEffect(() => {
if (isConnecting || !providerId) return
diff --git a/src/hooks/useHealthComputer.tsx b/src/hooks/useHealthComputer.tsx
index bbcd4119..93ca9b91 100644
--- a/src/hooks/useHealthComputer.tsx
+++ b/src/hooks/useHealthComputer.tsx
@@ -43,57 +43,66 @@ export default function useHealthComputer(account?: Account) {
const vaultPositionValues = useMemo(() => {
if (!account?.vaults) return null
- return account.vaults.reduce((prev, curr) => {
- const baseCoinPrice = prices.find((price) => price.denom === curr.denoms.lp)?.amount || 0
- prev[curr.address] = {
- base_coin: {
- amount: '0', // Not used by healthcomputer
- denom: curr.denoms.lp,
- value: curr.amounts.unlocking.times(baseCoinPrice).integerValue().toString(),
- },
- vault_coin: {
- amount: '0', // Not used by healthcomputer
- denom: curr.denoms.vault,
- value: curr.values.primary
- .div(baseCurrencyPrice)
- .plus(curr.values.secondary.div(baseCurrencyPrice))
- .integerValue()
- .toString(),
- },
- }
- return prev
- }, {} as { [key: string]: VaultPositionValue })
+ return account.vaults.reduce(
+ (prev, curr) => {
+ const baseCoinPrice = prices.find((price) => price.denom === curr.denoms.lp)?.amount || 0
+ prev[curr.address] = {
+ base_coin: {
+ amount: '0', // Not used by healthcomputer
+ denom: curr.denoms.lp,
+ value: curr.amounts.unlocking.times(baseCoinPrice).integerValue().toString(),
+ },
+ vault_coin: {
+ amount: '0', // Not used by healthcomputer
+ denom: curr.denoms.vault,
+ value: curr.values.primary
+ .div(baseCurrencyPrice)
+ .plus(curr.values.secondary.div(baseCurrencyPrice))
+ .integerValue()
+ .toString(),
+ },
+ }
+ return prev
+ },
+ {} as { [key: string]: VaultPositionValue },
+ )
}, [account?.vaults, prices, baseCurrencyPrice])
const priceData = useMemo(() => {
const baseCurrencyPrice =
prices.find((price) => price.denom === baseCurrency.denom)?.amount || 0
- return prices.reduce((prev, curr) => {
- prev[curr.denom] = curr.amount.div(baseCurrencyPrice).decimalPlaces(18).toString()
- return prev
- }, {} as { [key: string]: string })
+ return prices.reduce(
+ (prev, curr) => {
+ prev[curr.denom] = curr.amount.div(baseCurrencyPrice).decimalPlaces(18).toString()
+ return prev
+ },
+ {} as { [key: string]: string },
+ )
}, [prices, baseCurrency.denom])
const denomsData = useMemo(
() =>
- assetParams.reduce((prev, curr) => {
- const params: AssetParamsBaseForAddr = {
- ...curr,
- // The following overrides are required as testnet is 'broken' and new contracts are not updated yet
- // These overrides are not used by the healthcomputer internally, so they're not important anyways.
- protocol_liquidation_fee: '1',
- liquidation_bonus: {
- max_lb: '1',
- min_lb: '1',
- slope: '1',
- starting_lb: '1',
- },
- }
- prev[params.denom] = params
+ assetParams.reduce(
+ (prev, curr) => {
+ const params: AssetParamsBaseForAddr = {
+ ...curr,
+ // The following overrides are required as testnet is 'broken' and new contracts are not updated yet
+ // These overrides are not used by the healthcomputer internally, so they're not important anyways.
+ protocol_liquidation_fee: '1',
+ liquidation_bonus: {
+ max_lb: '1',
+ min_lb: '1',
+ slope: '1',
+ starting_lb: '1',
+ },
+ }
+ prev[params.denom] = params
- return prev
- }, {} as { [key: string]: AssetParamsBaseForAddr }),
+ return prev
+ },
+ {} as { [key: string]: AssetParamsBaseForAddr },
+ ),
[assetParams],
)
@@ -103,10 +112,13 @@ export default function useHealthComputer(account?: Account) {
const vaultPositionDenoms = positions.vaults.map((vault) => vault.vault.address)
return vaultConfigs
.filter((config) => vaultPositionDenoms.includes(config.addr))
- .reduce((prev, curr) => {
- prev[curr.addr] = curr
- return prev
- }, {} as { [key: string]: VaultConfigBaseForString })
+ .reduce(
+ (prev, curr) => {
+ prev[curr.addr] = curr
+ return prev
+ },
+ {} as { [key: string]: VaultConfigBaseForString },
+ )
}, [vaultConfigs, positions])
const healthComputer: HealthComputer | null = useMemo(() => {
diff --git a/src/store/slices/broadcast.ts b/src/store/slices/broadcast.ts
index 619b9cf8..4938d919 100644
--- a/src/store/slices/broadcast.ts
+++ b/src/store/slices/broadcast.ts
@@ -363,6 +363,7 @@ export default function createBroadcastSlice(
fee: StdFee
accountId: string
coinIn: BNCoin
+ borrow?: BNCoin
denomOut: string
slippage: number
}) => {
@@ -370,6 +371,7 @@ export default function createBroadcastSlice(
update_credit_account: {
account_id: options.accountId,
actions: [
+ ...(options.borrow ? [{ borrow: options.borrow.toCoin() }] : []),
{
swap_exact_in: {
coin_in: options.coinIn.toActionCoin(),
diff --git a/src/types/interfaces/store/broadcast.d.ts b/src/types/interfaces/store/broadcast.d.ts
index e952ec31..a0aa113e 100644
--- a/src/types/interfaces/store/broadcast.d.ts
+++ b/src/types/interfaces/store/broadcast.d.ts
@@ -61,6 +61,7 @@ interface BroadcastSlice {
fee: StdFee
accountId: string
coinIn: BNCoin
+ borrow: BNCoin
denomOut: string
slippage: number
}) => Promise
diff --git a/src/utils/accounts.ts b/src/utils/accounts.ts
index a0fe4b18..58674b75 100644
--- a/src/utils/accounts.ts
+++ b/src/utils/accounts.ts
@@ -100,7 +100,7 @@ export function convertAccountToPositions(account: Account): Positions {
],
},
},
- } as VaultPosition),
+ }) as VaultPosition,
),
}
}
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index c5b23cf2..3462231a 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -1,4 +1,5 @@
import BigNumber from 'bignumber.js'
+import throttle from 'lodash.throttle'
BigNumber.config({ EXPONENTIAL_AT: 1e9 })
export function BN(n: BigNumber.Value) {
@@ -10,3 +11,15 @@ export function getApproximateHourlyInterest(amount: string, borrowRate: number)
.dividedBy(24 * 365)
.multipliedBy(amount)
}
+
+export function asyncThrottle Promise>(func: F, wait?: number) {
+ const throttled = throttle((resolve, reject, args: Parameters) => {
+ func(...args)
+ .then(resolve)
+ .catch(reject)
+ }, wait)
+ return (...args: Parameters): ReturnType =>
+ new Promise((resolve, reject) => {
+ throttled(resolve, reject, args)
+ }) as ReturnType
+}
diff --git a/yarn.lock b/yarn.lock
index ddd292f8..27059d50 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3564,6 +3564,13 @@
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
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":
version "4.3.7"
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"
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:
version "4.3.0"
resolved "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz"