+ For your convenience, token approval and order placement will be bundled into a single transaction, streamlining
+ your experience!
+
)
}
@@ -27,11 +28,12 @@ export type BundleTxWrapBannerProps = {
export function BundleTxWrapBanner({ nativeCurrencySymbol, wrappedCurrencySymbol }: BundleTxWrapBannerProps) {
return (
- <>
- Token wrapping: For your convenience, CoW Swap will bundle all the necessary actions for this
- trade into a single transaction. This includes the {nativeCurrencySymbol} wrapping and, if needed,{' '}
+ Token wrapping bundling
+
+ For your convenience, CoW Swap will bundle all the necessary actions for this trade into a single transaction.
+ This includes the {nativeCurrencySymbol} wrapping and, if needed,
{wrappedCurrencySymbol} approval. Even if the trade fails, your wrapping and approval will be done!
- >
+
)
}
@@ -48,13 +50,14 @@ export function BundleTxSafeWcBanner({ nativeCurrencySymbol, supportsWrapping }:
return (
- <>
+ Use Safe web app
+
Use the Safe web app for streamlined trading: {supportsWrappingText}token approval and orders bundled in one go!
Only available in the{' '}
CoW Swap Safe App↗
- >
+
)
}
@@ -67,17 +70,18 @@ export type SmallVolumeWarningBannerProps = {
export function SmallVolumeWarningBanner({ feePercentage, feeAmount }: SmallVolumeWarningBannerProps) {
return (
- <>
- Small orders are unlikely to be executed. For this order, network fees would be{' '}
+ Small orders are unlikely to be executed
+
+ For this order, network fees would be{' '}
{feePercentage?.toFixed(2)}% (
)
{' '}
of your sell amount! Therefore, your order is unlikely to execute.
- {/* */}
- {/* TODO: add link to somewhere */}
- {/*Learn more ↗*/}
- >
+
+ {/* */}
+ {/* TODO: add link to somewhere */}
+ {/*Learn more ↗*/}
)
}
diff --git a/src/common/pure/InlineBanner/index.cosmos.tsx b/src/common/pure/InlineBanner/index.cosmos.tsx
index dda1795d9c..f1ea3565a3 100644
--- a/src/common/pure/InlineBanner/index.cosmos.tsx
+++ b/src/common/pure/InlineBanner/index.cosmos.tsx
@@ -20,10 +20,11 @@ const Fixtures = {
information: (
- <>
- Token approval: For your convenience, token approval and order placement will be bundled into
- a single transaction, streamlining your experience!
- >
+ Token approval bundling
+
+ For your convenience, token approval and order placement will be bundled into a single transaction,
+ streamlining your experience!
+
),
@@ -44,10 +45,11 @@ const Fixtures = {
smallVolumeWarning: (
- <>
- Small orders are unlikely to be executed. For this order, network fees would be 1.00% (2500 COW) of
- your sell amount! Therefore, your order is unlikely to execute.
- >
+ Small orders are unlikely to be executed
+
+ For this order, network fees would be 1.00% (2500 COW) of your sell amount! Therefore, your order is
+ unlikely to execute.
+
- TWAP order split in {numberOfPartsValue} equal parts
+
+ TWAP order split in {numberOfPartsValue} equal parts
{/* Sell amount per part */}
{totalDurationDisplay}
-
+ Unsupported Safe detected
-
- Connected Safe lacks required fallback handler. Switch to a compatible Safe or modify fallback handler for
- TWAP orders when placing your order.{' '}
+
+ Connected Safe lacks required fallback handler. Switch to a compatible Safe or modify fallback handler for
+ TWAP orders when placing your order.
+
{/*Learn more*/}
{/*TODO: set a proper link*/}
{fallbackHandlerCheckbox}
-
+
)
}
}
diff --git a/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartTimeWarning.tsx b/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartTimeWarning.tsx
index 3196bee934..9f7d177e55 100644
--- a/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartTimeWarning.tsx
+++ b/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartTimeWarning.tsx
@@ -8,10 +8,11 @@ export function SmallPartTimeWarning() {
return (
- <>
- Minimum part time: A minimum of {time} between parts is required. Decrease the
- number of parts or increase the total duration.
- >
+ Insufficient time between parts
+
+ A minimum of {time} between parts is required. Decrease the number of parts or increase the
+ total duration.
+
)
}
diff --git a/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartVolumeWarning.tsx b/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartVolumeWarning.tsx
index 8c9b900233..1d6483ef9f 100644
--- a/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartVolumeWarning.tsx
+++ b/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartVolumeWarning.tsx
@@ -14,13 +14,14 @@ export function SmallPartVolumeWarning({ chainId }: SmallPartVolumeWarningBanner
return (
- <>
- Minimum sell size: The sell amount per part of your TWAP order should be at least{' '}
-
+ Minimum sell size
+
+ The sell amount per part of your TWAP order should be at least{' '}
+
$
-
+
. Decrease the number of parts or increase the total sell amount.
- >
+
)
}
diff --git a/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx b/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx
index 3723d46d9b..854e2d8d38 100644
--- a/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx
+++ b/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx
@@ -6,12 +6,14 @@ export function UnsupportedWalletWarning({ isSafeViaWc }: { isSafeViaWc: boolean
if (isSafeViaWc) {
return (
- <>
- Use the Safe web app for advanced trading. Only available in the{' '}
+ Use Safe web app
+
+ Use the Safe web app for advanced trading.
+ Only available in the{' '}
CoW Swap Safe App↗
- >
+
)
}
@@ -19,11 +21,11 @@ export function UnsupportedWalletWarning({ isSafeViaWc }: { isSafeViaWc: boolean
return (
Unsupported wallet detected
-
- TWAP orders currently require a Safe with a special fallback handler.
- Have one? Switch to it! {/*Need setup? Click here.*/}{' '}
-
- Future updates may extend wallet support!
+
+ TWAP orders currently require a Safe with a special fallback handler. Have one? Switch to it!{' '}
+ {/*Need setup? Click here.*/}Future updates may
+ extend wallet support!
+
{/*TODO: set a proper link*/}
)
diff --git a/src/modules/twap/containers/TwapFormWidget/index.tsx b/src/modules/twap/containers/TwapFormWidget/index.tsx
index 3685317f48..f88f17ce7b 100644
--- a/src/modules/twap/containers/TwapFormWidget/index.tsx
+++ b/src/modules/twap/containers/TwapFormWidget/index.tsx
@@ -1,6 +1,6 @@
import { useAtomValue } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'
-import { useEffect } from 'react'
+import { useEffect, useState } from 'react'
import {
openAdvancedOrdersTabAnalytics,
@@ -20,7 +20,9 @@ import { TwapFormState } from 'modules/twap/pure/PrimaryActionButton/getTwapForm
import { QuoteObserverUpdater } from 'modules/twap/updaters/QuoteObserverUpdater'
import { useIsSafeApp, useWalletInfo } from 'modules/wallet'
+import { usePrice } from 'common/hooks/usePrice'
import { useRateInfoParams } from 'common/hooks/useRateInfoParams'
+import { ExecutionPrice } from 'common/pure/ExecutionPrice'
import * as styledEl from './styled'
import { AMOUNT_PARTS_LABELS, LABELS_TOOLTIPS } from './tooltips'
@@ -35,7 +37,7 @@ import { useTwapFormState } from '../../hooks/useTwapFormState'
import { AmountParts } from '../../pure/AmountParts'
import { DeadlineSelector } from '../../pure/DeadlineSelector'
import { partsStateAtom } from '../../state/partsStateAtom'
-import { twapTimeIntervalAtom } from '../../state/twapOrderAtom'
+import { twapSlippageAdjustedBuyAmount, twapTimeIntervalAtom } from '../../state/twapOrderAtom'
import {
twapOrderSlippageAtom,
twapOrdersSettingsAtom,
@@ -57,6 +59,7 @@ export function TwapFormWidget() {
const { numberOfPartsValue, slippageValue, deadline, customDeadline, isCustomDeadline } =
useAtomValue(twapOrdersSettingsAtom)
+ const buyAmount = useAtomValue(twapSlippageAdjustedBuyAmount)
const { inputCurrencyAmount, outputCurrencyAmount } = useAdvancedOrdersDerivedState()
const { inputCurrencyAmount: rawInputCurrencyAmount } = useAdvancedOrdersRawState()
@@ -78,6 +81,8 @@ export function TwapFormWidget() {
const rateInfoParams = useRateInfoParams(inputCurrencyAmount, outputCurrencyAmount)
+ const limitPrice = usePrice(inputCurrencyAmount, buyAmount)
+
const deadlineState = {
deadline,
customDeadline,
@@ -110,6 +115,9 @@ export function TwapFormWidget() {
}
}, [account, isFallbackHandlerRequired, isFallbackHandlerCompatible, localFormValidation, verification])
+ const isInvertedState = useState(false)
+ const [isInverted] = isInvertedState
+
return (
<>
@@ -123,10 +131,32 @@ export function TwapFormWidget() {
{!isWrapOrUnwrap && (
-
+
)}
-
+ updateSettingsState({ slippageValue: value })}
+ decimalsPlaces={2}
+ placeholder={DEFAULT_TWAP_SLIPPAGE.toFixed(1)}
+ max={50}
+ label={LABELS_TOOLTIPS.slippage.label}
+ tooltip={renderTooltip(LABELS_TOOLTIPS.slippage.tooltip)}
+ prefixComponent={
+
+ {limitPrice ? (
+
+ ) : (
+ '0'
+ )}
+
+ }
+ suffix="%"
+ />
- updateSettingsState({ slippageValue: value })}
- decimalsPlaces={2}
- placeholder={DEFAULT_TWAP_SLIPPAGE.toFixed(0)}
- max={50}
- label={LABELS_TOOLTIPS.slippage.label}
- tooltip={renderTooltip(LABELS_TOOLTIPS.slippage.tooltip)}
- suffix="%"
- />
-
-
+
+
theme.mediaWidth.upToSmall`
flex-direction: column;
@@ -31,3 +32,10 @@ export const StyledRateInfo = styled(RateInfo)`
display: grid;
grid-template-columns: max-content auto;
`
+
+export const StyledPriceProtection = styled.div`
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ width: 100%;
+`
diff --git a/src/modules/twap/containers/TwapFormWidget/tooltips.tsx b/src/modules/twap/containers/TwapFormWidget/tooltips.tsx
index 2c5f79dd73..2b56f49a5a 100644
--- a/src/modules/twap/containers/TwapFormWidget/tooltips.tsx
+++ b/src/modules/twap/containers/TwapFormWidget/tooltips.tsx
@@ -1,7 +1,24 @@
+import SVG from 'react-inlinesvg'
+import styled from 'styled-components/macro'
+
+import ShieldImage from 'legacy/assets/cow-swap/protection.svg'
+
import { deadlinePartsDisplay } from 'modules/twap/utils/deadlinePartsDisplay'
+const IconImage = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ > svg {
+ opacity: 0.5;
+ fill: ${({ theme }) => theme.text1};
+ margin: 0 3px 0 0;
+ }
+`
+
export interface LabelTooltip {
- label: string
+ label: React.ReactNode
tooltip?: React.ReactNode | ((params: any) => React.ReactNode)
}
@@ -42,14 +59,17 @@ export const LABELS_TOOLTIPS: LabelTooltipItems = {
),
},
slippage: {
- label: 'Slippage',
+ label: (
+ <>
+
+
+ {' '}
+ Price protection
+ >
+ ),
tooltip: (
<>
- This slippage will apply to each part of your order. Since a TWAP order executes over a longer period of time,
- your slippage should take into account possible price fluctuations over that time.
-
-
- If your slippage is too low, you risk some parts of your order failing to execute.
+ Your TWAP order won't execute and is protected if the market price dips more than your set slippage tolerance.
>
),
},
diff --git a/src/modules/twap/pure/AmountParts/styled.tsx b/src/modules/twap/pure/AmountParts/styled.tsx
index 4c18b6d69a..e753097f32 100644
--- a/src/modules/twap/pure/AmountParts/styled.tsx
+++ b/src/modules/twap/pure/AmountParts/styled.tsx
@@ -36,13 +36,15 @@ export const Label = styled(TradeWidgetFieldLabel)`
`
export const Amount = styled(TokenAmount)`
+ display: flex;
+ flex-flow: row wrap;
font-size: 18px;
font-weight: 500;
padding: 2px 0;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
- display: flex;
+ gap: 5px;
> div:first-child {
margin-right: 5px;
diff --git a/src/modules/twap/pure/CustomDeadlineSelector/styled.tsx b/src/modules/twap/pure/CustomDeadlineSelector/styled.tsx
index 35e187cf4a..cbb2963b65 100644
--- a/src/modules/twap/pure/CustomDeadlineSelector/styled.tsx
+++ b/src/modules/twap/pure/CustomDeadlineSelector/styled.tsx
@@ -40,6 +40,7 @@ export const ModalFooter = styled.div`
export const ModalContent = styled.div`
display: flex;
+ flex-flow: row wrap;
grid-gap: 6px;
`
diff --git a/src/modules/twap/state/twapOrderAtom.ts b/src/modules/twap/state/twapOrderAtom.ts
index 0d117d8bd6..3158b0d4d6 100644
--- a/src/modules/twap/state/twapOrderAtom.ts
+++ b/src/modules/twap/state/twapOrderAtom.ts
@@ -19,22 +19,35 @@ export const twapTimeIntervalAtom = atom((get) => {
return seconds / numberOfPartsValue
})
+/**
+ * Get slippage adjusted buyAmount for TWAP orders
+ *
+ * Calculated independently as we don't need the user to be connected to know how much they would receive
+ */
+export const twapSlippageAdjustedBuyAmount = atom | null>((get) => {
+ const { outputCurrencyAmount } = get(advancedOrdersDerivedStateAtom)
+
+ if (!outputCurrencyAmount) return null
+
+ const slippage = get(twapOrderSlippageAtom)
+
+ const slippageAmount = outputCurrencyAmount.multiply(slippage)
+ return outputCurrencyAmount.subtract(slippageAmount) as CurrencyAmount
+})
+
export const twapOrderAtom = atom((get) => {
const appDataInfo = get(appDataInfoAtom)
const { account } = get(walletInfoAtom)
const { numberOfPartsValue } = get(twapOrdersSettingsAtom)
const timeInterval = get(twapTimeIntervalAtom)
- const { inputCurrencyAmount, outputCurrencyAmount, recipient } = get(advancedOrdersDerivedStateAtom)
- const slippage = get(twapOrderSlippageAtom)
+ const { inputCurrencyAmount, recipient } = get(advancedOrdersDerivedStateAtom)
+ const buyAmount = get(twapSlippageAdjustedBuyAmount)
- if (!inputCurrencyAmount || !outputCurrencyAmount || !account) return null
-
- const slippageAmount = outputCurrencyAmount.multiply(slippage)
- const buyAmountWithSlippage = outputCurrencyAmount.subtract(slippageAmount)
+ if (!inputCurrencyAmount || !buyAmount || !account) return null
return {
sellAmount: inputCurrencyAmount as CurrencyAmount,
- buyAmount: buyAmountWithSlippage as CurrencyAmount,
+ buyAmount,
receiver: recipient || account, // TODO: check case with ENS name
numOfParts: numberOfPartsValue,
startTime: 0, // Will be set to a block timestamp value from CurrentBlockTimestampFactory