diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfig.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfig.tsx
index 619fac02dfb..9e24401d1c2 100644
--- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfig.tsx
+++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfig.tsx
@@ -8,11 +8,12 @@ import {
MenuItemOption,
MenuList,
MenuOptionGroup,
+ Skeleton,
Stack,
} from '@chakra-ui/react'
import type { Asset } from '@shapeshiftoss/types'
import { bn, bnOrZero } from '@shapeshiftoss/utils'
-import { useCallback, useMemo, useRef, useState } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { NumberFormatValues } from 'react-number-format'
import NumberFormat from 'react-number-format'
import { StyledAssetMenuButton } from 'components/AssetSelection/components/AssetMenuButton'
@@ -77,22 +78,25 @@ const getExpiryOptionTranslation = (expiryOption: ExpiryOption) => {
const timePeriodRightIcon =
const swapIcon =
-const swapPriceButtonProps = { pr: 4 }
+const disabledProps = { opacity: 0.5, cursor: 'not-allowed', userSelect: 'none' }
+const swapPriceButtonProps = { pr: 4, _disabled: disabledProps }
type LimitOrderConfigProps = {
sellAsset: Asset
buyAsset: Asset
- marketPriceBuyAssetCryptoPrecision: string
- limitPriceBuyAssetCryptoPrecision: string
- setLimitPriceBuyAssetCryptoPrecision: (priceBuyAssetCryptoPrecision: string) => void
+ isLoading: boolean
+ marketPriceBuyAsset: string
+ limitPriceBuyAsset: string
+ setLimitPriceBuyAsset: (newLimitPriceBuyAsset: string) => void
}
export const LimitOrderConfig = ({
sellAsset,
buyAsset,
- marketPriceBuyAssetCryptoPrecision,
- limitPriceBuyAssetCryptoPrecision,
- setLimitPriceBuyAssetCryptoPrecision,
+ isLoading,
+ marketPriceBuyAsset,
+ limitPriceBuyAsset,
+ setLimitPriceBuyAsset,
}: LimitOrderConfigProps) => {
const priceAmountRef = useRef(null)
@@ -100,6 +104,20 @@ export const LimitOrderConfig = ({
const [presetLimit, setPresetLimit] = useState(PresetLimit.Market)
const [expiryOption, setExpiryOption] = useState(ExpiryOption.SevenDays)
+ // Reset the user config when the assets change
+ useEffect(
+ () => {
+ setPriceDirection(PriceDirection.Default)
+ setPresetLimit(PresetLimit.Market)
+ setExpiryOption(ExpiryOption.SevenDays)
+ setLimitPriceBuyAsset(marketPriceBuyAsset)
+ },
+ // NOTE: we DO NOT want to react to `marketPriceBuyAsset` here, because polling will reset it
+ // every time!
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [sellAsset, buyAsset, setLimitPriceBuyAsset],
+ )
+
const {
number: { localeParts },
} = useLocaleFormatter()
@@ -120,14 +138,14 @@ export const LimitOrderConfig = ({
// Lower the decimal places when the integer is greater than 8 significant digits for better UI
const priceCryptoFormatted = useMemo(() => {
- const cryptoAmountIntegerCount = bnOrZero(
- bnOrZero(limitPriceBuyAssetCryptoPrecision).toFixed(0),
- ).precision(true)
+ const cryptoAmountIntegerCount = bnOrZero(bnOrZero(limitPriceBuyAsset).toFixed(0)).precision(
+ true,
+ )
return cryptoAmountIntegerCount <= 8
- ? limitPriceBuyAssetCryptoPrecision
- : bnOrZero(limitPriceBuyAssetCryptoPrecision).toFixed(3)
- }, [limitPriceBuyAssetCryptoPrecision])
+ ? limitPriceBuyAsset
+ : bnOrZero(limitPriceBuyAsset).toFixed(3)
+ }, [limitPriceBuyAsset])
const arrow = useMemo(() => {
return priceDirection === PriceDirection.Default ? '↑' : '↓'
@@ -152,14 +170,14 @@ export const LimitOrderConfig = ({
assertUnreachable(presetLimit)
}
})()
- const adjustedLimitPrice = bn(marketPriceBuyAssetCryptoPrecision).times(multiplier).toFixed()
+ const adjustedLimitPrice = bn(marketPriceBuyAsset).times(multiplier).toFixed()
const maybeReversedPrice =
priceDirection === PriceDirection.Reversed
? bn(1).div(adjustedLimitPrice).toFixed()
: adjustedLimitPrice
- setLimitPriceBuyAssetCryptoPrecision(maybeReversedPrice)
+ setLimitPriceBuyAsset(maybeReversedPrice)
},
- [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision],
+ [marketPriceBuyAsset, setLimitPriceBuyAsset],
)
const handleSetMarketLimit = useCallback(() => {
@@ -191,37 +209,31 @@ export const LimitOrderConfig = ({
if (isCustomLimit) {
// For custom limit, just take the reciprocal as we don't know what the original input value was
- setLimitPriceBuyAssetCryptoPrecision(bn(1).div(limitPriceBuyAssetCryptoPrecision).toFixed())
+ setLimitPriceBuyAsset(bn(1).div(limitPriceBuyAsset).toFixed())
} else {
// Otherwise set it to the precise value based on the original market price
handleSetPresetLimit(presetLimit, newPriceDirection)
}
- }, [
- handleSetPresetLimit,
- limitPriceBuyAssetCryptoPrecision,
- presetLimit,
- priceDirection,
- setLimitPriceBuyAssetCryptoPrecision,
- ])
+ }, [handleSetPresetLimit, limitPriceBuyAsset, presetLimit, priceDirection, setLimitPriceBuyAsset])
const handlePriceChange = useCallback(() => {
// onChange will send us the formatted value
// To get around this we need to get the value from the onChange using a ref
// Now when the max buttons are clicked the onChange will not fire
- setLimitPriceBuyAssetCryptoPrecision(priceAmountRef.current ?? '0')
+ setLimitPriceBuyAsset(priceAmountRef.current ?? '0')
// Unset the preset limit, as this is a custom value
setPresetLimit(undefined)
- }, [setLimitPriceBuyAssetCryptoPrecision])
+ }, [setLimitPriceBuyAsset])
const handleValueChange = useCallback(
(values: NumberFormatValues) => {
// This fires anytime value changes including setting it on max click
// Store the value in a ref to send when we actually want the onChange to fire
priceAmountRef.current = values.value
- setLimitPriceBuyAssetCryptoPrecision(values.value)
+ setLimitPriceBuyAsset(values.value)
},
- [setLimitPriceBuyAssetCryptoPrecision],
+ [setLimitPriceBuyAsset],
)
const expiryOptionTranslation = useMemo(() => {
@@ -239,7 +251,7 @@ export const LimitOrderConfig = ({
-
+
+
+
@@ -280,6 +295,7 @@ export const LimitOrderConfig = ({
size='sm'
isActive={presetLimit === PresetLimit.Market}
onClick={handleSetMarketLimit}
+ isDisabled={isLoading}
>
Market
@@ -288,6 +304,7 @@ export const LimitOrderConfig = ({
size='sm'
isActive={presetLimit === PresetLimit.OnePercent}
onClick={handleSetOnePercentLimit}
+ isDisabled={isLoading}
>
1% {arrow}
@@ -296,6 +313,7 @@ export const LimitOrderConfig = ({
size='sm'
isActive={presetLimit === PresetLimit.TwoPercent}
onClick={handleSetTwoPercentLimit}
+ isDisabled={isLoading}
>
2% {arrow}
@@ -304,6 +322,7 @@ export const LimitOrderConfig = ({
size='sm'
isActive={presetLimit === PresetLimit.FivePercent}
onClick={handleSetFivePercentLimit}
+ isDisabled={isLoading}
>
5% {arrow}
@@ -312,6 +331,7 @@ export const LimitOrderConfig = ({
size='sm'
isActive={presetLimit === PresetLimit.TenPercent}
onClick={handleSetTenPercentLimit}
+ isDisabled={isLoading}
>
10% {arrow}
diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx
index 55ae21a67e3..52916d80f70 100644
--- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx
+++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx
@@ -1,9 +1,9 @@
-import { Divider, Stack, useMediaQuery } from '@chakra-ui/react'
+import { Divider, Stack } from '@chakra-ui/react'
import { skipToken } from '@reduxjs/toolkit/query'
import { foxAssetId, fromAccountId, usdcAssetId } from '@shapeshiftoss/caip'
import { SwapperName } from '@shapeshiftoss/swapper'
import type { Asset } from '@shapeshiftoss/types'
-import { bnOrZero, toBaseUnit } from '@shapeshiftoss/utils'
+import { BigNumber, bn, bnOrZero, fromBaseUnit, toBaseUnit } from '@shapeshiftoss/utils'
import type { FormEvent } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useFormContext } from 'react-hook-form'
@@ -15,6 +15,7 @@ import { WalletActions } from 'context/WalletProvider/actions'
import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast'
import { useWallet } from 'hooks/useWallet/useWallet'
import { localAssetData } from 'lib/asset-service'
+import { calculateFees } from 'lib/fees/model'
import type { ParameterModel } from 'lib/fees/parameters/types'
import { useQuoteLimitOrderQuery } from 'state/apis/limit-orders/limitOrderApi'
import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors'
@@ -23,16 +24,18 @@ import {
selectFirstAccountIdByChainId,
selectIsAnyAccountMetadataLoadedForChainId,
selectMarketDataByAssetIdUserCurrency,
+ selectUsdRateByAssetId,
+ selectUserCurrencyToUsdRate,
} from 'state/slices/selectors'
import {
- selectActiveQuote,
- // selectBuyAmountAfterFeesUserCurrency,
+ selectCalculatedFees,
+ selectDefaultSlippagePercentage,
selectIsTradeQuoteRequestAborted,
selectShouldShowTradeQuoteOrAwaitInput,
} from 'state/slices/tradeQuoteSlice/selectors'
import { useAppSelector } from 'state/store'
-import { breakpoints } from 'theme/theme'
+import { SharedSlippagePopover } from '../../SharedTradeInput/SharedSlippagePopover'
import { SharedTradeInput } from '../../SharedTradeInput/SharedTradeInput'
import { SharedTradeInputBody } from '../../SharedTradeInput/SharedTradeInputBody'
import { SharedTradeInputFooter } from '../../SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter'
@@ -43,6 +46,7 @@ import { LimitOrderBuyAsset } from './LimitOrderBuyAsset'
import { LimitOrderConfig } from './LimitOrderConfig'
const votingPowerParams: { feeModel: ParameterModel } = { feeModel: 'SWAPPER' }
+const thorVotingPowerParams: { feeModel: ParameterModel } = { feeModel: 'THORSWAP' }
type LimitOrderInputProps = {
tradeInputRef: React.MutableRefObject
@@ -63,14 +67,18 @@ export const LimitOrderInput = ({
const history = useHistory()
const { handleSubmit } = useFormContext()
const { showErrorToast } = useErrorHandler()
- const [isSmallerThanXl] = useMediaQuery(`(max-width: ${breakpoints.xl})`, { ssr: false })
+ const [userSlippagePercentage, setUserSlippagePercentage] = useState()
const [sellAsset, setSellAsset] = useState(localAssetData[usdcAssetId] ?? defaultAsset)
const [buyAsset, setBuyAsset] = useState(localAssetData[foxAssetId] ?? defaultAsset)
+ const [limitPriceBuyAsset, setLimitPriceBuyAsset] = useState('0')
const defaultAccountId = useAppSelector(state =>
selectFirstAccountIdByChainId(state, sellAsset.chainId),
)
+ const defaultSlippagePercentage = useAppSelector(state =>
+ selectDefaultSlippagePercentage(state, SwapperName.CowSwap),
+ )
const [buyAccountId, setBuyAccountId] = useState(defaultAccountId)
const [sellAccountId, setSellAccountId] = useState(defaultAccountId)
@@ -86,13 +94,14 @@ export const LimitOrderInput = ({
const [isConfirmationLoading, setIsConfirmationLoading] = useState(false)
const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false)
const [sellAmountCryptoPrecision, setSellAmountCryptoPrecision] = useState('0')
- // const buyAmountAfterFeesUserCurrency = useAppSelector(selectBuyAmountAfterFeesUserCurrency)
const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput)
const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending)
const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted)
const hasUserEnteredAmount = true
const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams))
- const activeQuote = useAppSelector(selectActiveQuote)
+ const thorVotingPower = useAppSelector(state => selectVotingPower(state, thorVotingPowerParams))
+ const sellAssetUsdRate = useAppSelector(state => selectUsdRateByAssetId(state, sellAsset.assetId))
+ const userCurrencyRate = useAppSelector(selectUserCurrencyToUsdRate)
const isAnyAccountMetadataLoadedForChainIdFilter = useMemo(
() => ({ chainId: sellAsset.chainId }),
[sellAsset.chainId],
@@ -101,53 +110,32 @@ export const LimitOrderInput = ({
selectIsAnyAccountMetadataLoadedForChainId(state, isAnyAccountMetadataLoadedForChainIdFilter),
)
- const handleOpenCompactQuoteList = useCallback(() => {
- if (!isCompact && !isSmallerThanXl) return
- history.push({ pathname: LimitOrderRoutePaths.QuoteList })
- }, [history, isCompact, isSmallerThanXl])
-
const isVotingPowerLoading = useMemo(
() => isSnapshotApiQueriesPending && votingPower === undefined,
[isSnapshotApiQueriesPending, votingPower],
)
- const isLoading = useMemo(
- () =>
- // No account meta loaded for that chain
- !isAnyAccountMetadataLoadedForChainId ||
- (!shouldShowTradeQuoteOrAwaitInput && !isTradeQuoteRequestAborted) ||
- isConfirmationLoading ||
- // Only consider snapshot API queries as pending if we don't have voting power yet
- // if we do, it means we have persisted or cached (both stale) data, which is enough to let the user continue
- // as we are optimistic and don't want to be waiting for a potentially very long time for the snapshot API to respond
- isVotingPowerLoading,
- [
- isAnyAccountMetadataLoadedForChainId,
- shouldShowTradeQuoteOrAwaitInput,
- isTradeQuoteRequestAborted,
- isConfirmationLoading,
- isVotingPowerLoading,
- ],
- )
-
- const assetMarketDataUserCurrency = useAppSelector(state =>
+ const sellAssetMarketDataUserCurrency = useAppSelector(state =>
selectMarketDataByAssetIdUserCurrency(state, sellAsset.assetId),
)
const sellAmountUserCurrency = useMemo(() => {
- return bnOrZero(sellAmountCryptoPrecision).times(assetMarketDataUserCurrency.price).toFixed()
- }, [assetMarketDataUserCurrency.price, sellAmountCryptoPrecision])
+ return bnOrZero(sellAmountCryptoPrecision)
+ .times(sellAssetMarketDataUserCurrency.price)
+ .toFixed()
+ }, [sellAssetMarketDataUserCurrency.price, sellAmountCryptoPrecision])
+
+ const sellAmountUsd = useMemo(() => {
+ return bnOrZero(sellAmountCryptoPrecision)
+ .times(sellAssetUsdRate ?? '0')
+ .toFixed()
+ }, [sellAmountCryptoPrecision, sellAssetUsdRate])
const warningAcknowledgementMessage = useMemo(() => {
// TODO: Implement me
return ''
}, [])
- const headerRightContent = useMemo(() => {
- // TODO: Implement me
- return <>>
- }, [])
-
const handleSwitchAssets = useCallback(() => {
setSellAsset(buyAsset)
setBuyAsset(sellAsset)
@@ -221,6 +209,19 @@ export const LimitOrderInput = ({
return fromAccountId(sellAccountId).account as Address
}, [sellAccountId])
+ const affiliateBps = useMemo(() => {
+ const tradeAmountUsd = bnOrZero(sellAssetUsdRate).times(sellAmountCryptoPrecision)
+
+ const { feeBps } = calculateFees({
+ tradeAmountUsd,
+ foxHeld: bnOrZero(votingPower),
+ thorHeld: bnOrZero(thorVotingPower),
+ feeModel: 'SWAPPER',
+ })
+
+ return feeBps.toFixed(0)
+ }, [sellAmountCryptoPrecision, sellAssetUsdRate, thorVotingPower, votingPower])
+
const limitOrderQuoteParams = useMemo(() => {
// Return skipToken if any required params are missing
if (bnOrZero(sellAmountCryptoBaseUnit).isZero()) {
@@ -231,40 +232,81 @@ export const LimitOrderInput = ({
sellAssetId: sellAsset.assetId,
buyAssetId: buyAsset.assetId,
chainId: sellAsset.chainId,
- slippageTolerancePercentageDecimal: '0', // TODO: wire this up!
- affiliateBps: '0', // TODO: wire this up!
+ slippageTolerancePercentageDecimal: bn(userSlippagePercentage ?? defaultSlippagePercentage)
+ .div(100)
+ .toString(),
+ affiliateBps,
sellAccountAddress,
sellAmountCryptoBaseUnit,
recipientAddress,
}
}, [
- buyAsset.assetId,
- sellAccountAddress,
sellAmountCryptoBaseUnit,
sellAsset.assetId,
sellAsset.chainId,
+ buyAsset.assetId,
+ userSlippagePercentage,
+ defaultSlippagePercentage,
+ affiliateBps,
+ sellAccountAddress,
recipientAddress,
])
- const { data, error } = useQuoteLimitOrderQuery(limitOrderQuoteParams)
-
+ // This fetches the quote only, not the limit order. The quote is used to determine the market
+ // price. When submitting a limit order, the buyAmount is (optionally) modified based on the user
+ // input, and then re-attached to the `LimitOrder` before signing and submitting via our
+ // `placeLimitOrder` endpoint in limitOrderApi
+ const {
+ data,
+ error,
+ isFetching: isLimitOrderQuoteFetching,
+ } = useQuoteLimitOrderQuery(limitOrderQuoteParams)
+
+ const marketPriceBuyAsset = useMemo(() => {
+ if (!data) return '0'
+ return bnOrZero(fromBaseUnit(data.quote.buyAmount, buyAsset.precision))
+ .div(fromBaseUnit(data.quote.sellAmount, sellAsset.precision))
+ .toFixed()
+ }, [buyAsset.precision, data, sellAsset.precision])
+
+ // Reset the limit price when the market price changes.
+ // TODO: If we introduce polling of quotes, we will need to add logic inside `LimitOrderConfig` to
+ // not reset the user's config unless the asset pair changes.
useEffect(() => {
- /*
- This is the quote only, not the limit order. The quote is used to determine the market price.
- When submitting a limit order, the buyAmount is (optionally) modified based on the user input,
- and then re-attached to the `LimitOrder` before signing and submitting via our
- `placeLimitOrder` endpoint in limitOrderApi
- */
- console.log('limit order quote response:', data)
- console.log('limit order quote error:', error)
- }, [data, error])
-
- const marketPriceBuyAssetCryptoPrecision = '123423'
-
- // TODO: debounce this with `useDebounce` when including in the query
- const [limitPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision] = useState(
- marketPriceBuyAssetCryptoPrecision,
- )
+ setLimitPriceBuyAsset(marketPriceBuyAsset)
+ }, [marketPriceBuyAsset])
+
+ const isLoading = useMemo(() => {
+ return (
+ isLimitOrderQuoteFetching ||
+ // No account meta loaded for that chain
+ !isAnyAccountMetadataLoadedForChainId ||
+ (!shouldShowTradeQuoteOrAwaitInput && !isTradeQuoteRequestAborted) ||
+ isConfirmationLoading ||
+ // Only consider snapshot API queries as pending if we don't have voting power yet
+ // if we do, it means we have persisted or cached (both stale) data, which is enough to let the user continue
+ // as we are optimistic and don't want to be waiting for a potentially very long time for the snapshot API to respond
+ isVotingPowerLoading
+ )
+ }, [
+ isAnyAccountMetadataLoadedForChainId,
+ isConfirmationLoading,
+ isLimitOrderQuoteFetching,
+ isTradeQuoteRequestAborted,
+ isVotingPowerLoading,
+ shouldShowTradeQuoteOrAwaitInput,
+ ])
+
+ const headerRightContent = useMemo(() => {
+ return (
+
+ )
+ }, [defaultSlippagePercentage, userSlippagePercentage])
const bodyContent = useMemo(() => {
return (
@@ -294,64 +336,73 @@ export const LimitOrderInput = ({
)
}, [
+ buyAccountId,
buyAsset,
isInputtingFiatSellAmount,
isLoading,
+ limitPriceBuyAsset,
+ marketPriceBuyAsset,
+ sellAccountId,
sellAmountCryptoPrecision,
sellAmountUserCurrency,
sellAsset,
- sellAccountId,
- handleSwitchAssets,
- handleSetSellAsset,
- setSellAccountId,
- buyAccountId,
- setBuyAccountId,
handleSetBuyAsset,
- limitPriceBuyAssetCryptoPrecision,
+ handleSetSellAsset,
+ handleSwitchAssets,
])
+ const { feeUsd } = useAppSelector(state =>
+ selectCalculatedFees(state, { feeModel: 'SWAPPER', inputAmountUsd: sellAmountUsd }),
+ )
+
+ const affiliateFeeAfterDiscountUserCurrency = useMemo(() => {
+ return bn(feeUsd).times(userCurrencyRate).toFixed(2, BigNumber.ROUND_HALF_UP)
+ }, [feeUsd, userCurrencyRate])
+
const footerContent = useMemo(() => {
return (
{renderedRecipientAddress}
)
}, [
+ affiliateBps,
+ affiliateFeeAfterDiscountUserCurrency,
buyAsset,
- handleOpenCompactQuoteList,
hasUserEnteredAmount,
- isCompact,
+ sellAmountUsd,
+ error,
isLoading,
- activeQuote?.rate,
- sellAsset,
+ limitPriceBuyAsset,
sellAccountId,
isRecipientAddressEntryActive,
+ sellAsset,
renderedRecipientAddress,
])
diff --git a/src/components/MultiHopTrade/components/RateGasRow.tsx b/src/components/MultiHopTrade/components/RateGasRow.tsx
index 60af29ec9a9..f1297441ec7 100644
--- a/src/components/MultiHopTrade/components/RateGasRow.tsx
+++ b/src/components/MultiHopTrade/components/RateGasRow.tsx
@@ -1,46 +1,28 @@
import { ArrowUpDownIcon, ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'
import type { FlexProps } from '@chakra-ui/react'
-import {
- Box,
- Center,
- Collapse,
- Flex,
- Skeleton,
- Stack,
- Tooltip,
- useDisclosure,
-} from '@chakra-ui/react'
+import { Box, Collapse, Flex, Skeleton, Stack, Tooltip, useDisclosure } from '@chakra-ui/react'
import type { SwapperName, SwapSource } from '@shapeshiftoss/swapper'
-import {
- THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE,
- THORCHAIN_STREAM_SWAP_SOURCE,
-} from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/constants'
-import { AnimatePresence } from 'framer-motion'
import type { PropsWithChildren } from 'react'
-import { type FC, memo, useMemo } from 'react'
+import { type FC, memo } from 'react'
import { FaGasPump } from 'react-icons/fa'
import { useTranslate } from 'react-polyglot'
import { Amount } from 'components/Amount/Amount'
import { HelperTooltip } from 'components/HelperTooltip/HelperTooltip'
-import { StreamIcon } from 'components/Icons/Stream'
import { Row } from 'components/Row/Row'
-import { SlideTransitionX } from 'components/SlideTransitionX'
import { Text } from 'components/Text'
-import { bnOrZero } from 'lib/bignumber/bignumber'
-import { firstNonZeroDecimal } from 'lib/math'
-import { SwapperIcon } from './TradeInput/components/SwapperIcon/SwapperIcon'
+import { SwapperIcons } from './SwapperIcons'
type RateGasRowProps = {
- sellSymbol?: string
- buySymbol?: string
- rate?: string
- gasFee: string
+ buyAssetSymbol: string
+ isDisabled?: boolean
isLoading?: boolean
- allowSelectQuote: boolean
- swapperName?: SwapperName
- swapSource?: SwapSource
- onRateClick?: () => void
+ rate: string | undefined
+ sellAssetSymbol: string
+ swapperName: SwapperName | undefined
+ swapSource: SwapSource | undefined
+ totalNetworkFeeFiatPrecision: string | undefined
+ onClick?: () => void
} & PropsWithChildren
const helpersTooltipFlexProps: FlexProps = { flexDirection: 'row-reverse' }
@@ -52,56 +34,20 @@ const rateHover = {
export const RateGasRow: FC = memo(
({
- sellSymbol,
- buySymbol,
- rate,
- gasFee,
+ buyAssetSymbol,
+ children,
+ isDisabled,
isLoading,
- allowSelectQuote,
+ rate,
+ sellAssetSymbol,
swapperName,
swapSource,
- onRateClick,
- children,
+ totalNetworkFeeFiatPrecision,
+ onClick,
}) => {
const translate = useTranslate()
const { isOpen, onToggle } = useDisclosure()
- const swapperIcons = useMemo(() => {
- const isStreaming =
- swapSource === THORCHAIN_STREAM_SWAP_SOURCE ||
- swapSource === THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE
- return (
-
- {isStreaming && (
-
-
-
-
-
- )}
-
- {swapperName && }
-
-
- )
- }, [swapSource, swapperName])
-
switch (true) {
case isLoading:
return (
@@ -140,10 +86,7 @@ export const RateGasRow: FC = memo(
fontSize='sm'
>
-
+
= memo(
display='flex'
alignItems='center'
gap={2}
- _hover={allowSelectQuote ? rateHover : undefined}
- onClick={onRateClick}
+ _hover={!isDisabled ? rateHover : undefined}
+ onClick={onClick}
>
- {swapperIcons}
+
= memo(
-
- {allowSelectQuote && }
+
+ {!isDisabled && }
@@ -188,7 +127,7 @@ export const RateGasRow: FC = memo(
-
+
{isOpen ? (
diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedSlippagePopover.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedSlippagePopover.tsx
new file mode 100644
index 00000000000..a2e0d042ddb
--- /dev/null
+++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedSlippagePopover.tsx
@@ -0,0 +1,212 @@
+import {
+ Alert,
+ AlertDescription,
+ AlertIcon,
+ Box,
+ Button,
+ ButtonGroup,
+ FormControl,
+ IconButton,
+ Input,
+ InputGroup,
+ InputRightElement,
+ Popover,
+ PopoverBody,
+ PopoverContent,
+ PopoverTrigger,
+ Tooltip,
+} from '@chakra-ui/react'
+import { bnOrZero } from '@shapeshiftoss/chain-adapters'
+import type { FC } from 'react'
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { FaGear } from 'react-icons/fa6'
+import { useTranslate } from 'react-polyglot'
+import { HelperTooltip } from 'components/HelperTooltip/HelperTooltip'
+import { Row } from 'components/Row/Row'
+import { Text } from 'components/Text'
+import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag'
+
+enum SlippageType {
+ Auto = 'Auto',
+ Custom = 'Custom',
+}
+
+const maxSlippagePercentage = '30'
+
+const focusStyle = { '&[aria-invalid=true]': { borderColor: 'red.500' } }
+
+const faGear =
+
+type SharedSlippagePopoverProps = {
+ defaultSlippagePercentage: string
+ isDisabled?: boolean
+ quoteSlippagePercentage: string | undefined
+ tooltipTranslation?: string
+ userSlippagePercentage: string | undefined
+ setUserSlippagePercentage: (slippagePercentage: string | undefined) => void
+}
+
+export const SharedSlippagePopover: FC = memo(
+ ({
+ defaultSlippagePercentage,
+ isDisabled,
+ quoteSlippagePercentage,
+ tooltipTranslation,
+ userSlippagePercentage,
+ setUserSlippagePercentage,
+ }) => {
+ const [slippageType, setSlippageType] = useState(SlippageType.Auto)
+ const [slippageAmount, setSlippageAmount] = useState(
+ defaultSlippagePercentage,
+ )
+ const [isInvalid, setIsInvalid] = useState(false)
+ const translate = useTranslate()
+ const inputRef = useRef(null)
+ const isAdvancedSlippageEnabled = useFeatureFlag('AdvancedSlippage')
+
+ useEffect(() => {
+ // Handles re-opening the slippage popover and/or going back to input step
+ if (userSlippagePercentage) {
+ setSlippageType(SlippageType.Custom)
+ setSlippageAmount(userSlippagePercentage)
+ } else {
+ setSlippageType(SlippageType.Auto)
+ setSlippageAmount(quoteSlippagePercentage ?? defaultSlippagePercentage)
+ }
+ // We only want this to run on mount, though not to be reactive to userSlippagePercentage
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [defaultSlippagePercentage, quoteSlippagePercentage])
+
+ const handleChange = useCallback((e: React.ChangeEvent) => {
+ const value = e.target.value
+ if (bnOrZero(value).gt(maxSlippagePercentage)) {
+ setIsInvalid(true)
+ } else {
+ setIsInvalid(false)
+ }
+ setSlippageAmount(value)
+ setSlippageType(SlippageType.Custom)
+ }, [])
+
+ const handleClose = useCallback(() => {
+ if (slippageType === SlippageType.Custom && !isInvalid)
+ setUserSlippagePercentage(slippageAmount)
+ else if (slippageType === SlippageType.Auto) setUserSlippagePercentage(undefined)
+ }, [setUserSlippagePercentage, isInvalid, slippageAmount, slippageType])
+
+ const handleSlippageTypeChange = useCallback(
+ (type: SlippageType) => {
+ if (type === SlippageType.Auto) {
+ setSlippageAmount(defaultSlippagePercentage)
+ setIsInvalid(false)
+ } else {
+ inputRef && inputRef.current && inputRef.current.focus()
+ setSlippageAmount(slippageAmount)
+ }
+ setSlippageType(type)
+ },
+ [defaultSlippagePercentage, slippageAmount],
+ )
+
+ const handleAutoSlippageTypeChange = useCallback(
+ () => handleSlippageTypeChange(SlippageType.Auto),
+ [handleSlippageTypeChange],
+ )
+
+ const handleCustomSlippageTypeChange = useCallback(
+ () => handleSlippageTypeChange(SlippageType.Custom),
+ [handleSlippageTypeChange],
+ )
+
+ const isHighSlippage = useMemo(() => bnOrZero(slippageAmount).gt(1), [slippageAmount])
+ const isLowSlippage = useMemo(() => bnOrZero(slippageAmount).lt(0.05), [slippageAmount])
+
+ if (!isAdvancedSlippageEnabled) return null
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {slippageType === SlippageType.Auto && 'Auto'}
+
+
+
+
+
+
+
+
+
+
+
+
+ %
+
+
+
+
+ {isHighSlippage && (
+
+
+
+ {translate('trade.slippage.warning')}
+
+
+ )}
+ {isLowSlippage && (
+
+
+
+ {translate('trade.slippage.lowSlippage')}
+
+
+ )}
+
+
+
+ )
+ },
+)
diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx
index 385cd8542cc..0a713e17d96 100644
--- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx
+++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter.tsx
@@ -1,4 +1,4 @@
-import { CardFooter, useMediaQuery } from '@chakra-ui/react'
+import { CardFooter } from '@chakra-ui/react'
import type { SwapperName, SwapSource } from '@shapeshiftoss/swapper'
import type { Asset } from '@shapeshiftoss/types'
import type { InterpolationOptions } from 'node-polyglot'
@@ -9,7 +9,6 @@ import { Text } from 'components/Text'
import { useAccountsFetchQuery } from 'context/AppProvider/hooks/useAccountsFetchQuery'
import { selectFeeAssetById } from 'state/slices/selectors'
import { useAppSelector } from 'state/store'
-import { breakpoints } from 'theme/theme'
import { ReceiveSummary } from './components/ReceiveSummary'
@@ -20,19 +19,19 @@ type SharedTradeInputFooterProps = {
children?: JSX.Element
hasUserEnteredAmount: boolean
inputAmountUsd: string | undefined
- isCompact: boolean | undefined
isError: boolean
isLoading: boolean
quoteStatusTranslation: string | [string, InterpolationOptions]
rate: string | undefined
- sellAsset: Asset
+ receiveSummaryDetails?: JSX.Element | null
sellAccountId: string | undefined
+ sellAsset: Asset
+ shouldDisableGasRateRowClick?: boolean
shouldDisablePreviewButton: boolean | undefined
swapperName: SwapperName | undefined
swapSource: SwapSource | undefined
- totalNetworkFeeFiatPrecision: string
- receiveSummaryDetails?: JSX.Element | null
- onRateClick: () => void
+ totalNetworkFeeFiatPrecision: string | undefined
+ onGasRateRowClick?: () => void
}
export const SharedTradeInputFooter = ({
@@ -42,22 +41,20 @@ export const SharedTradeInputFooter = ({
children,
hasUserEnteredAmount,
inputAmountUsd,
- isCompact,
isError,
isLoading: isParentLoading,
quoteStatusTranslation,
rate,
- sellAsset,
+ receiveSummaryDetails,
sellAccountId,
+ sellAsset,
+ shouldDisableGasRateRowClick,
shouldDisablePreviewButton: parentShouldDisablePreviewButton,
swapperName,
swapSource,
totalNetworkFeeFiatPrecision,
- receiveSummaryDetails,
- onRateClick,
+ onGasRateRowClick,
}: SharedTradeInputFooterProps) => {
- const [isSmallerThanXl] = useMediaQuery(`(max-width: ${breakpoints.xl})`, { ssr: false })
-
const buyAssetFeeAsset = useAppSelector(state =>
selectFeeAssetById(state, buyAsset?.assetId ?? ''),
)
@@ -101,15 +98,15 @@ export const SharedTradeInputFooter = ({
>
{hasUserEnteredAmount && (
+import { SharedSlippagePopover } from './SharedTradeInput/SharedSlippagePopover'
type SlippagePopoverProps = {
isDisabled?: boolean
@@ -51,165 +17,27 @@ type SlippagePopoverProps = {
export const SlippagePopover: FC = memo(
({ tooltipTranslation, isDisabled }) => {
+ const dispatch = useAppDispatch()
const defaultSlippagePercentage = useAppSelector(selectDefaultSlippagePercentage)
const quoteSlippagePercentage = useAppSelector(selectQuoteSlippageTolerancePercentage)
-
const userSlippagePercentage = useAppSelector(selectUserSlippagePercentage)
- const [slippageType, setSlippageType] = useState(SlippageType.Auto)
- const [slippageAmount, setSlippageAmount] = useState(
- defaultSlippagePercentage,
- )
- const [isInvalid, setIsInvalid] = useState(false)
- const translate = useTranslate()
- const inputRef = useRef(null)
- const isAdvancedSlippageEnabled = useFeatureFlag('AdvancedSlippage')
- const dispatch = useAppDispatch()
-
- useEffect(() => {
- // Handles re-opening the slippage popover and/or going back to input step
- if (userSlippagePercentage) {
- setSlippageType(SlippageType.Custom)
- setSlippageAmount(userSlippagePercentage)
- } else {
- setSlippageType(SlippageType.Auto)
- setSlippageAmount(quoteSlippagePercentage ?? defaultSlippagePercentage)
- }
- // We only want this to run on mount, though not to be reactive to userSlippagePercentage
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [defaultSlippagePercentage, quoteSlippagePercentage])
-
- const handleChange = useCallback((e: React.ChangeEvent) => {
- const value = e.target.value
- if (bnOrZero(value).gt(maxSlippagePercentage)) {
- setIsInvalid(true)
- } else {
- setIsInvalid(false)
- }
- setSlippageAmount(value)
- setSlippageType(SlippageType.Custom)
- }, [])
-
- const handleClose = useCallback(() => {
- if (slippageType === SlippageType.Custom && !isInvalid)
- dispatch(tradeInput.actions.setSlippagePreferencePercentage(slippageAmount))
- else if (slippageType === SlippageType.Auto)
- dispatch(tradeInput.actions.setSlippagePreferencePercentage(undefined))
- }, [dispatch, isInvalid, slippageAmount, slippageType])
-
- const handleSlippageTypeChange = useCallback(
- (type: SlippageType) => {
- if (type === SlippageType.Auto) {
- setSlippageAmount(defaultSlippagePercentage)
- setIsInvalid(false)
- } else {
- inputRef && inputRef.current && inputRef.current.focus()
- setSlippageAmount(slippageAmount)
- }
- setSlippageType(type)
+ const setSlippagePreferencePercentage = useCallback(
+ (slippagePercentage: string | undefined) => {
+ dispatch(tradeInput.actions.setSlippagePreferencePercentage(slippagePercentage))
},
- [defaultSlippagePercentage, slippageAmount],
- )
-
- const handleAutoSlippageTypeChange = useCallback(
- () => handleSlippageTypeChange(SlippageType.Auto),
- [handleSlippageTypeChange],
+ [dispatch],
)
- const handleCustomSlippageTypeChange = useCallback(
- () => handleSlippageTypeChange(SlippageType.Custom),
- [handleSlippageTypeChange],
- )
-
- const isHighSlippage = useMemo(() => bnOrZero(slippageAmount).gt(1), [slippageAmount])
- const isLowSlippage = useMemo(() => bnOrZero(slippageAmount).lt(0.05), [slippageAmount])
-
- if (!isAdvancedSlippageEnabled) return null
-
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {slippageType === SlippageType.Auto && 'Auto'}
-
-
-
-
-
-
-
-
-
-
-
-
- %
-
-
-
-
- {isHighSlippage && (
-
-
-
- {translate('trade.slippage.warning')}
-
-
- )}
- {isLowSlippage && (
-
-
-
- {translate('trade.slippage.lowSlippage')}
-
-
- )}
-
-
-
+
)
},
)
diff --git a/src/components/MultiHopTrade/components/SwapperIcons.tsx b/src/components/MultiHopTrade/components/SwapperIcons.tsx
new file mode 100644
index 00000000000..98f299c1bd2
--- /dev/null
+++ b/src/components/MultiHopTrade/components/SwapperIcons.tsx
@@ -0,0 +1,52 @@
+import { Center } from '@chakra-ui/react'
+import type { SwapperName, SwapSource } from '@shapeshiftoss/swapper'
+import {
+ THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE,
+ THORCHAIN_STREAM_SWAP_SOURCE,
+} from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/constants'
+import { AnimatePresence } from 'framer-motion'
+import { StreamIcon } from 'components/Icons/Stream'
+import { SlideTransitionX } from 'components/SlideTransitionX'
+
+import { SwapperIcon } from './TradeInput/components/SwapperIcon/SwapperIcon'
+
+type SwapperIconsProps = {
+ swapSource: SwapSource | undefined
+ swapperName: SwapperName | undefined
+}
+
+export const SwapperIcons = ({ swapSource, swapperName }: SwapperIconsProps) => {
+ const isStreaming =
+ swapSource === THORCHAIN_STREAM_SWAP_SOURCE ||
+ swapSource === THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE
+ return (
+
+ {isStreaming && (
+
+
+
+
+
+ )}
+
+ {swapperName && }
+
+
+ )
+}
diff --git a/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx b/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx
index 5de4688a4ef..7a3820b0c35 100644
--- a/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx
+++ b/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx
@@ -363,24 +363,24 @@ export const ConfirmSummary = ({
return (
<>
{nativeAssetBridgeWarning ? (
diff --git a/src/state/apis/limit-orders/limitOrderApi.ts b/src/state/apis/limit-orders/limitOrderApi.ts
index 7bf175c7d4e..6a2eff9dd99 100644
--- a/src/state/apis/limit-orders/limitOrderApi.ts
+++ b/src/state/apis/limit-orders/limitOrderApi.ts
@@ -21,6 +21,7 @@ import type { Address } from 'viem'
import { zeroAddress } from 'viem'
import { BASE_RTK_CREATE_API_CONFIG } from '../const'
+import type { LimitOrderQuoteResponse } from './types'
import {
type CancelLimitOrdersRequest,
type CompetitionOrderStatus,
@@ -50,7 +51,7 @@ export const limitOrderApi = createApi({
keepUnusedDataFor: Number.MAX_SAFE_INTEGER, // never clear, we will manage this
tagTypes: ['LimitOrder'],
endpoints: build => ({
- quoteLimitOrder: build.query({
+ quoteLimitOrder: build.query({
queryFn: async ({
sellAssetId,
buyAssetId,
@@ -97,7 +98,7 @@ export const limitOrderApi = createApi({
limitOrderQuoteRequest.appDataHash = appDataHash
try {
- const result = await axios.post(
+ const result = await axios.post(
`${baseUrl}/${network}/api/v1/quote/`,
limitOrderQuoteRequest,
)
diff --git a/src/state/apis/limit-orders/types.ts b/src/state/apis/limit-orders/types.ts
index 01936356b07..fd334b10701 100644
--- a/src/state/apis/limit-orders/types.ts
+++ b/src/state/apis/limit-orders/types.ts
@@ -5,6 +5,7 @@ import type {
CoWSwapSellTokenSource,
CoWSwapSigningScheme,
} from '@shapeshiftoss/swapper'
+import type { Address } from 'viem'
export enum PriceQuality {
Fast = 'fast',
@@ -30,6 +31,33 @@ export type LimitOrderQuoteRequest = {
sellAmountBeforeFee: string
}
+export type LimitOrderQuote = {
+ sellToken: AssetReference
+ buyToken: AssetReference
+ receiver?: string
+ sellAmount: string
+ buyAmount: string
+ validTo: number
+ feeAmount: string
+ kind: CoWSwapOrderKind
+ partiallyFillable: boolean
+ sellTokenBalance?: CoWSwapSellTokenSource
+ buyTokenBalance?: CoWSwapBuyTokenDestination
+ signingScheme: CoWSwapSigningScheme
+ signature: string
+ from?: string
+ appData: string
+ appDataHash?: string
+}
+
+export type LimitOrderQuoteResponse = {
+ expiration: string
+ from: Address
+ id: LimitOrderQuoteId
+ quote: LimitOrderQuote
+ verified: boolean
+}
+
export type LimitOrder = {
sellToken: AssetReference
buyToken: AssetReference
diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts
index 014de3dbd6f..db34ad921e2 100644
--- a/src/state/slices/tradeQuoteSlice/selectors.ts
+++ b/src/state/slices/tradeQuoteSlice/selectors.ts
@@ -555,6 +555,8 @@ type AffiliateFeesProps = {
inputAmountUsd: string | undefined
}
+// TODO: Move out of tradeQuoteSlice as this is used for limit orders also, and is not spot
+// specific
export const selectCalculatedFees: Selector =
createCachedSelector(
(_state: ReduxState, { feeModel }: AffiliateFeesProps) => feeModel,