From ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2 Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 17 Oct 2024 11:19:05 +0100 Subject: [PATCH] feat(widget): deadline widget param (#4991) * feat: add deadlines widget parameter * refactor: remove dead code * feat: rename OrderDeadlines to ForcedOrderDeadline and use FlexibleConfig * feat: add useInjectedWidgetDeadline * fix: add spaces to tooltip * feat: use widget deadline on swap form * chore: remove duplicated css property * feat: use widget deadline on limit form * feat: use widget deadline on twap form * chore: remove debug logs * chore: fix build * fix: round timestamp * refactor: rename limitOrdersDeadlines to LIMIT_ORDERS_DEADLINES * fix: use deadlineMilliseconds instead of customDeadline for forcedOrderDeadline * fix: allow deadline input to be cleared * fix: add chainId to hook deps --- .../components/TransactionSettings/index.tsx | 39 ++++++++++--- .../src/legacy/state/user/hooks.tsx | 17 +++--- .../hooks/useInjectedWidgetDeadline.ts | 33 +++++++++++ .../src/modules/injectedWidget/index.ts | 1 + .../containers/DeadlineInput/index.tsx | 47 ++++++++++++--- .../pure/DeadlineSelector/deadlines.ts | 27 ++++++++- .../pure/DeadlineSelector/index.cosmos.tsx | 1 + .../pure/DeadlineSelector/index.tsx | 58 +++++++++++-------- .../updaters/AlternativeLimitOrderUpdater.ts | 6 +- .../swap/pure/Row/RowDeadline/index.tsx | 3 + .../modules/trade/pure/TradeSelect/index.tsx | 3 +- .../twap/containers/TwapFormWidget/index.tsx | 41 ++++++++++++- .../twap/pure/DeadlineSelector/index.tsx | 40 +++++++++---- .../configurator/controls/DeadlineControl.tsx | 23 ++++++++ .../hooks/useWidgetParamsAndSettings.ts | 14 ++++- .../src/app/configurator/index.tsx | 24 ++++++++ .../src/app/configurator/types.ts | 4 ++ libs/common-const/src/misc.ts | 1 - libs/widget-lib/src/types.ts | 12 ++++ 19 files changed, 324 insertions(+), 70 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts create mode 100644 apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx index 388726d36c..7ad9ea8175 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useRef, useState } from 'react' +import { useCallback, useContext, useEffect, useRef, useState } from 'react' import { DEFAULT_DEADLINE_FROM_NOW, @@ -16,6 +16,7 @@ import { useOnClickOutside } from '@cowprotocol/common-hooks' import { getWrappedToken, percentToBps } from '@cowprotocol/common-utils' import { FancyButton, HelpTooltip, Media, RowBetween, RowFixed, UI } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' +import { TradeType } from '@cowprotocol/widget-lib' import { Percent } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' @@ -27,6 +28,7 @@ import { AutoColumn } from 'legacy/components/Column' import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { orderExpirationTimeAnalytics, slippageToleranceAnalytics } from 'modules/analytics' +import { useInjectedWidgetDeadline } from 'modules/injectedWidget' import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useIsSlippageModified } from 'modules/swap/hooks/useIsSlippageModified' import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' @@ -191,6 +193,7 @@ export function TransactionSettings() { const chosenSlippageMatchesSmartSlippage = smartSlippage && new Percent(smartSlippage, 10_000).equalTo(swapSlippage) const [deadline, setDeadline] = useUserTransactionTTL() + const widgetDeadline = useInjectedWidgetDeadline(TradeType.SWAP) const [slippageInput, setSlippageInput] = useState('') const [slippageError, setSlippageError] = useState(false) @@ -249,6 +252,12 @@ export function TransactionSettings() { new Percent(isEoaEthFlow ? HIGH_ETH_FLOW_SLIPPAGE_BPS : smartSlippage || HIGH_SLIPPAGE_BPS, 10_000), ) + const minDeadline = isEoaEthFlow + ? // 10 minute low threshold for eth flow + MINIMUM_ETH_FLOW_DEADLINE_SECONDS + : MINIMUM_ORDER_VALID_TO_TIME_SECONDS + const maxDeadline = MAX_DEADLINE_MINUTES * 60 + const parseCustomDeadline = useCallback( (value: string) => { // populate what the user typed and clear the error @@ -263,12 +272,8 @@ export function TransactionSettings() { const parsed: number = Math.floor(Number.parseFloat(value) * 60) if ( !Number.isInteger(parsed) || // Check deadline is a number - parsed < - (isEoaEthFlow - ? // 10 minute low threshold for eth flow - MINIMUM_ETH_FLOW_DEADLINE_SECONDS - : MINIMUM_ORDER_VALID_TO_TIME_SECONDS) || // Check deadline is not too small - parsed > MAX_DEADLINE_MINUTES * 60 // Check deadline is not too big + parsed < minDeadline || // Check deadline is not too small + parsed > maxDeadline // Check deadline is not too big ) { setDeadlineError(DeadlineError.InvalidInput) } else { @@ -281,9 +286,26 @@ export function TransactionSettings() { } } }, - [isEoaEthFlow], + [minDeadline, maxDeadline], ) + useEffect(() => { + if (widgetDeadline) { + // Deadline is stored in seconds + const value = Math.floor(widgetDeadline) * 60 + + if (value < minDeadline) { + setDeadline(minDeadline) + } else if (value > maxDeadline) { + setDeadline(maxDeadline) + } else { + setDeadline(value) + } + } + }, [widgetDeadline, minDeadline, maxDeadline]) + + const isDeadlineDisabled = !!widgetDeadline + const showCustomDeadlineRow = Boolean(chainId) const onSlippageInputBlur = useCallback(() => { @@ -417,6 +439,7 @@ export function TransactionSettings() { setDeadlineError(false) }} color={deadlineError ? 'red' : ''} + disabled={isDeadlineDisabled} /> diff --git a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx index 01fa1094ad..f347ddb113 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx +++ b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import { L2_DEADLINE_FROM_NOW, NATIVE_CURRENCIES, SupportedLocale, TokenWithLogo } from '@cowprotocol/common-const' +import { NATIVE_CURRENCIES, SupportedLocale, TokenWithLogo } from '@cowprotocol/common-const' import { getIsNativeToken } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' @@ -20,11 +20,12 @@ export function useIsDarkMode(): boolean { userDarkMode, matchesDarkMode, }), - shallowEqual + shallowEqual, ) return userDarkMode === null ? matchesDarkMode : userDarkMode } + export function useDarkModeManager(): [boolean, Command] { const dispatch = useAppDispatch() const darkMode = useIsDarkMode() @@ -48,7 +49,7 @@ export function useUserLocaleManager(): [SupportedLocale | null, (newLocale: Sup (newLocale: SupportedLocale) => { dispatch(updateUserLocale({ userLocale: newLocale })) }, - [dispatch] + [dispatch], ) return [locale, setLocale] @@ -67,7 +68,7 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void (recipient: string | null) => { dispatch(setRecipient({ recipient })) }, - [dispatch] + [dispatch], ) const toggleVisibility = useCallback( @@ -78,7 +79,7 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void onChangeRecipient(null) } }, - [isVisible, dispatch, onChangeRecipient] + [isVisible, dispatch, onChangeRecipient], ) return [isVisible, toggleVisibility] @@ -86,15 +87,13 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void export function useUserTransactionTTL(): [number, (slippage: number) => void] { const dispatch = useAppDispatch() - const userDeadline = useAppSelector((state) => state.user.userDeadline) - const onL2 = false - const deadline = onL2 ? L2_DEADLINE_FROM_NOW : userDeadline + const deadline = useAppSelector((state) => state.user.userDeadline) const setUserDeadline = useCallback( (userDeadline: number) => { dispatch(updateUserDeadline({ userDeadline })) }, - [dispatch] + [dispatch], ) return [deadline, setUserDeadline] diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts b/apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts new file mode 100644 index 0000000000..c1ac9c2db9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts @@ -0,0 +1,33 @@ +import { useMemo } from 'react' + +import { isInjectedWidget } from '@cowprotocol/common-utils' +import { useWalletInfo } from '@cowprotocol/wallet' +import { ForcedOrderDeadline, resolveFlexibleConfig, SupportedChainId, TradeType } from '@cowprotocol/widget-lib' + +import { useInjectedWidgetParams } from './useInjectedWidgetParams' + +/** + * Returns the deadline set in the widget for the specific order type in minutes, if any + * + * Additional validation is needed + */ +export function useInjectedWidgetDeadline(tradeType: TradeType): number | undefined { + const { forcedOrderDeadline } = useInjectedWidgetParams() + const { chainId } = useWalletInfo() + + return useMemo(() => { + if (!isInjectedWidget()) { + return + } + + return getDeadline(forcedOrderDeadline, chainId, tradeType) + }, [tradeType, forcedOrderDeadline, chainId]) +} + +function getDeadline(deadline: ForcedOrderDeadline | undefined, chainId: SupportedChainId, tradeType: TradeType) { + if (!deadline) { + return + } + + return resolveFlexibleConfig(deadline, chainId, tradeType) +} diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/index.ts b/apps/cowswap-frontend/src/modules/injectedWidget/index.ts index 7190aa48c3..0b412e48b7 100644 --- a/apps/cowswap-frontend/src/modules/injectedWidget/index.ts +++ b/apps/cowswap-frontend/src/modules/injectedWidget/index.ts @@ -1,6 +1,7 @@ export { InjectedWidgetUpdater } from './updaters/InjectedWidgetUpdater' export { CowEventsUpdater } from './updaters/CowEventsUpdater' export { useInjectedWidgetParams, useWidgetPartnerFee } from './hooks/useInjectedWidgetParams' +export { useInjectedWidgetDeadline } from './hooks/useInjectedWidgetDeadline' export { useInjectedWidgetMetaData } from './hooks/useInjectedWidgetMetaData' export { useInjectedWidgetPalette } from './hooks/useInjectedWidgetPalette' export { injectedWidgetPartnerFeeAtom } from './state/injectedWidgetParamsAtom' diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx index a44991e3ee..5ed0816e20 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx @@ -1,9 +1,15 @@ -import { useSetAtom } from 'jotai' -import { useAtomValue } from 'jotai' -import { useCallback, useMemo, useRef } from 'react' +import { useAtomValue, useSetAtom } from 'jotai' +import { useCallback, useEffect, useMemo, useRef } from 'react' +import { TradeType } from '@cowprotocol/widget-lib' + +import { useInjectedWidgetDeadline } from 'modules/injectedWidget' import { DeadlineSelector } from 'modules/limitOrders/pure/DeadlineSelector' -import { LimitOrderDeadline, limitOrdersDeadlines } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' +import { + getLimitOrderDeadlines, + LIMIT_ORDERS_DEADLINES, + LimitOrderDeadline, +} from 'modules/limitOrders/pure/DeadlineSelector/deadlines' import { limitOrdersSettingsAtom, updateLimitOrdersSettingsAtom, @@ -13,29 +19,52 @@ export function DeadlineInput() { const { deadlineMilliseconds, customDeadlineTimestamp } = useAtomValue(limitOrdersSettingsAtom) const updateSettingsState = useSetAtom(updateLimitOrdersSettingsAtom) const currentDeadlineNode = useRef() - const existingDeadline = useMemo(() => { - return limitOrdersDeadlines.find((item) => item.value === deadlineMilliseconds) - }, [deadlineMilliseconds]) + const existingDeadline = useMemo( + () => getLimitOrderDeadlines(deadlineMilliseconds).find((item) => item.value === deadlineMilliseconds), + [deadlineMilliseconds], + ) + + const widgetDeadlineMinutes = useInjectedWidgetDeadline(TradeType.LIMIT) + + useEffect(() => { + if (widgetDeadlineMinutes) { + const widgetDeadlineDelta = widgetDeadlineMinutes * 60 * 1000 + const min = LIMIT_ORDERS_DEADLINES[0].value + const max = LIMIT_ORDERS_DEADLINES[LIMIT_ORDERS_DEADLINES.length - 1].value + + let deadlineMilliseconds = widgetDeadlineDelta + if (widgetDeadlineDelta < min) { + deadlineMilliseconds = min + } else if (widgetDeadlineDelta > max) { + deadlineMilliseconds = max + } + + updateSettingsState({ customDeadlineTimestamp: null, deadlineMilliseconds }) + } + }, [widgetDeadlineMinutes, updateSettingsState]) + + const isDeadlineDisabled = !!widgetDeadlineMinutes const selectDeadline = useCallback( (deadline: LimitOrderDeadline) => { updateSettingsState({ deadlineMilliseconds: deadline.value, customDeadlineTimestamp: null }) currentDeadlineNode.current?.click() // Close dropdown }, - [updateSettingsState] + [updateSettingsState], ) const selectCustomDeadline = useCallback( (customDeadline: number | null) => { updateSettingsState({ customDeadlineTimestamp: customDeadline }) }, - [updateSettingsState] + [updateSettingsState], ) return ( diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts index 3f183cf2cc..b0a04d3be0 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts @@ -1,4 +1,5 @@ import ms from 'ms.macro' +import { format } from 'timeago.js' import { MAX_ORDER_DEADLINE } from 'common/constants/common' @@ -12,7 +13,7 @@ export const MAX_CUSTOM_DEADLINE = MAX_ORDER_DEADLINE export const defaultLimitOrderDeadline: LimitOrderDeadline = { title: '7 Days', value: ms`7d` } -export const limitOrdersDeadlines: LimitOrderDeadline[] = [ +export const LIMIT_ORDERS_DEADLINES: LimitOrderDeadline[] = [ { title: '5 Minutes', value: ms`5m` }, { title: '30 Minutes', value: ms`30m` }, { title: '1 Hour', value: ms`1 hour` }, @@ -22,3 +23,27 @@ export const limitOrdersDeadlines: LimitOrderDeadline[] = [ { title: '1 Month', value: ms`30d` }, { title: '6 Months (max)', value: MAX_CUSTOM_DEADLINE }, ] + +/** + * Get limit order deadlines and optionally adds + * @param value + */ +export function getLimitOrderDeadlines(value?: number | LimitOrderDeadline): LimitOrderDeadline[] { + if (!value || LIMIT_ORDERS_DEADLINES.find((item) => item === value || item.value === value)) { + return LIMIT_ORDERS_DEADLINES + } + + const itemToAdd = typeof value === 'number' ? buildLimitOrderDeadline(value) : value + + return [...LIMIT_ORDERS_DEADLINES, itemToAdd].sort((a, b) => a.value - b.value) +} + +/** + * Builds a LimitOrderDeadline from milliseconds value. + * Uses timeago to an approximate title + */ +export function buildLimitOrderDeadline(value: number): LimitOrderDeadline { + const title = format(Date.now() + value, undefined).replace(/in /, '') + + return { title, value } +} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx index cb8d3b0b06..f0687b47e3 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx @@ -9,6 +9,7 @@ const Fixtures = { customDeadline={null} selectDeadline={() => void 0} selectCustomDeadline={() => void 0} + isDeadlineDisabled={false} /> ), } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx index 25fb8a440e..247d86c01f 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx @@ -16,7 +16,7 @@ import { import { CowModal as Modal } from 'common/pure/Modal' -import { LimitOrderDeadline, limitOrdersDeadlines } from './deadlines' +import { getLimitOrderDeadlines, LimitOrderDeadline } from './deadlines' import * as styledEl from './styled' const CUSTOM_DATE_OPTIONS: Intl.DateTimeFormatOptions = { @@ -31,12 +31,15 @@ const CUSTOM_DATE_OPTIONS: Intl.DateTimeFormatOptions = { export interface DeadlineSelectorProps { deadline: LimitOrderDeadline | undefined customDeadline: number | null + isDeadlineDisabled: boolean + selectDeadline(deadline: LimitOrderDeadline): void + selectCustomDeadline(deadline: number | null): void } export function DeadlineSelector(props: DeadlineSelectorProps) { - const { deadline, customDeadline, selectDeadline, selectCustomDeadline } = props + const { deadline, customDeadline, isDeadlineDisabled, selectDeadline, selectCustomDeadline } = props const currentDeadlineNode = useRef(null) const [[minDate, maxDate], setMinMax] = useState<[Date, Date]>(calculateMinMax) @@ -66,7 +69,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { } }, [maxDate, minDate, selectCustomDeadline, value]) - const existingDeadline = useMemo(() => limitOrdersDeadlines.find((item) => item === deadline), [deadline]) + const limitOrderDeadlines = useMemo(() => getLimitOrderDeadlines(deadline), [deadline]) const customDeadlineTitle = useMemo(() => { if (!customDeadline) { @@ -81,7 +84,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { selectCustomDeadline(null) // reset custom deadline currentDeadlineNode.current?.click() // Close dropdown }, - [selectCustomDeadline, selectDeadline] + [selectCustomDeadline, selectDeadline], ) // Sets value from input, if it exists @@ -92,7 +95,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { // In that case, use the default min value setValue(value || formatDateToLocalTime(minDate)) }, - [minDate] + [minDate], ) const [isOpen, setIsOpen] = useState(false) @@ -118,29 +121,38 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { onDismiss() }, [onDismiss, selectCustomDeadline, value]) + const deadlineDisplay = customDeadline ? customDeadlineTitle : deadline?.title + return ( Expiry - - - {customDeadline ? customDeadlineTitle : existingDeadline?.title} - - - - {limitOrdersDeadlines.map((item) => ( -
  • - setDeadline(item)}> - {item.title} - -
  • - ))} - - Custom - -
    -
    + + {isDeadlineDisabled ? ( +
    + {deadlineDisplay} +
    + ) : ( + + + {deadlineDisplay} + + + + {limitOrderDeadlines.map((item) => ( +
  • + setDeadline(item)}> + {item.title} + +
  • + ))} + + Custom + +
    +
    + )} {/* Custom deadline modal */} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts b/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts index 6045c3c672..a9cfc54141 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts @@ -15,7 +15,7 @@ import { updateLimitOrdersSettingsAtom, } from 'modules/limitOrders' import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' -import { limitOrdersDeadlines } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' +import { LIMIT_ORDERS_DEADLINES } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' import { partiallyFillableOverrideAtom } from 'modules/limitOrders/state/partiallyFillableOverride' import { useAlternativeOrder, useHideAlternativeOrderModal } from 'modules/trade/state/alternativeOrder' @@ -118,7 +118,7 @@ function useSetAlternativeRate(): null { // Set new active rate // The rate expects a raw fraction which is NOT a Price instace const activeRate = FractionUtils.fromPrice( - new Price({ baseAmount: inputCurrencyAmount, quoteAmount: outputCurrencyAmount }) + new Price({ baseAmount: inputCurrencyAmount, quoteAmount: outputCurrencyAmount }), ) updateRate({ activeRate, isTypedValue: false, isRateFromUrl: false, isAlternativeOrderRate: true }) @@ -170,7 +170,7 @@ function getDuration(order: Order | ParsedOrder): number { */ function getMatchingDeadline(duration: number) { // Match duration with approximate time - return limitOrdersDeadlines.find(({ value }) => { + return LIMIT_ORDERS_DEADLINES.find(({ value }) => { const ratio = value / duration // If the ratio is +/-10% off of 1, consider it a match return ratio > 0.9 && ratio < 1.1 diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx index d74a91ad0d..ac549413d6 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx @@ -20,10 +20,13 @@ export function getNativeOrderDeadlineTooltip(symbols: (string | undefined)[] | ) } + export function getNonNativeOrderDeadlineTooltip() { return ( Your swap expires and will not execute if it is pending for longer than the selected duration. +
    +
    {INPUT_OUTPUT_EXPLANATION}
    ) diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx index 591ce87d25..1d56933f36 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx @@ -1,6 +1,6 @@ import { UI } from '@cowprotocol/ui' -import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button' +import { Menu, MenuButton, MenuItem, MenuList } from '@reach/menu-button' import { ChevronDown } from 'react-feather' import styled from 'styled-components/macro' @@ -47,7 +47,6 @@ const StyledMenuButton = styled(MenuButton)` cursor: pointer; width: 100%; justify-content: space-between; - color: inherit; ` const StyledMenuItem = styled(MenuItem)` diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx index fb804210f3..41050dad54 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx @@ -3,9 +3,11 @@ import { useEffect, useLayoutEffect, useMemo, useState } from 'react' import { renderTooltip } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' +import { TradeType } from '@cowprotocol/widget-lib' import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders' import { openAdvancedOrdersTabAnalytics, twapWalletCompatibilityAnalytics } from 'modules/analytics' +import { useInjectedWidgetDeadline } from 'modules/injectedWidget' import { useReceiveAmountInfo } from 'modules/trade' import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap' import { useTradeState } from 'modules/trade/hooks/useTradeState' @@ -21,7 +23,14 @@ import { useRateInfoParams } from 'common/hooks/useRateInfoParams' import * as styledEl from './styled' import { LABELS_TOOLTIPS } from './tooltips' -import { DEFAULT_NUM_OF_PARTS, DEFAULT_TWAP_SLIPPAGE, MAX_TWAP_SLIPPAGE, ORDER_DEADLINES } from '../../const' +import { + DEFAULT_NUM_OF_PARTS, + DEFAULT_TWAP_SLIPPAGE, + MAX_PART_TIME, + MAX_TWAP_SLIPPAGE, + MINIMUM_PART_TIME, + ORDER_DEADLINES, +} from '../../const' import { useFallbackHandlerVerification, useIsFallbackHandlerCompatible, @@ -65,9 +74,34 @@ export function TwapFormWidget() { const limitPriceAfterSlippage = usePrice( receiveAmountInfo?.afterSlippage.sellAmount, - receiveAmountInfo?.afterSlippage.buyAmount + receiveAmountInfo?.afterSlippage.buyAmount, ) + const widgetDeadline = useInjectedWidgetDeadline(TradeType.ADVANCED) + + useEffect(() => { + if (widgetDeadline) { + // Ensure min part duration + const minDuration = Math.floor(MINIMUM_PART_TIME / 60) * 2 // it must have at least 2 parts + + const maxDuration = Math.floor(MAX_PART_TIME / 60) * numberOfPartsValue + + let minutes = widgetDeadline + if (widgetDeadline < minDuration) { + minutes = minDuration + } else if (widgetDeadline > maxDuration) { + minutes = maxDuration + } + + updateSettingsState({ + customDeadline: { hours: 0, minutes }, + isCustomDeadline: true, + }) + } + }, [widgetDeadline, updateSettingsState, numberOfPartsValue]) + + const isDeadlineDisabled = !!widgetDeadline + const deadlineState = { deadline, customDeadline, @@ -166,8 +200,9 @@ export function TwapFormWidget() { updateSettingsState(value)} + setDeadline={updateSettingsState} label={LABELS_TOOLTIPS.totalDuration.label} tooltip={renderTooltip(LABELS_TOOLTIPS.totalDuration.tooltip, { parts: numberOfPartsValue, diff --git a/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx b/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx index 8169b99b0a..6999fd7636 100644 --- a/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react' -import { UI } from '@cowprotocol/ui' -import { renderTooltip } from '@cowprotocol/ui' +import { renderTooltip, UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' @@ -10,14 +9,17 @@ import { Content } from 'modules/trade/pure/TradeWidgetField/styled' import { LabelTooltip } from 'modules/twap' import { customDeadlineToSeconds, deadlinePartsDisplay } from 'modules/twap/utils/deadlinePartsDisplay' +import { TradeWidgetField } from '../../../trade/pure/TradeWidgetField' import { defaultCustomDeadline, TwapOrdersDeadline } from '../../state/twapOrdersSettingsAtom' import { CustomDeadlineSelector } from '../CustomDeadlineSelector' interface DeadlineSelectorProps { items: TradeSelectItem[] deadline: TwapOrdersDeadline + isDeadlineDisabled: boolean label: LabelTooltip['label'] tooltip: LabelTooltip['tooltip'] + setDeadline(value: TwapOrdersDeadline): void } @@ -48,10 +50,22 @@ const StyledTradeSelect = styled(TradeSelect)` } ` +const StyledTradeField = styled(TradeWidgetField)` + ${Content} { + width: 100%; + color: inherit; + } + + ${Content} > div { + width: 100%; + } +` + export function DeadlineSelector(props: DeadlineSelectorProps) { const { items, deadline: { deadline, customDeadline, isCustomDeadline }, + isDeadlineDisabled, label, tooltip, setDeadline, @@ -74,7 +88,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { }) } }, - [setIsCustomModalOpen, setDeadline] + [setIsCustomModalOpen, setDeadline], ) const activeLabel = useMemo(() => { @@ -87,13 +101,19 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { return ( <> - + {isDeadlineDisabled ? ( + +
    {activeLabel}
    +
    + ) : ( + + )} setDeadline({ isCustomDeadline: true, customDeadline: value, deadline: 0 })} customDeadline={customDeadline} diff --git a/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx b/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx new file mode 100644 index 0000000000..57aff95d71 --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx @@ -0,0 +1,23 @@ +import { Dispatch, SetStateAction } from 'react' + +import { FormControl, TextField } from '@mui/material' + +export type DeadlineControlProps = { + label: string + deadlineState: [number | undefined, Dispatch>] +} + +export function DeadlineControl({ label, deadlineState: [state, setState] }: DeadlineControlProps) { + return ( + + setState(value && !isNaN(+value) ? Math.max(1, Number(value)) : undefined)} + size="small" + inputProps={{ min: 1 }} // Set minimum value to 1 + /> + + ) +} diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index b00d73748b..41c736f6d9 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' -import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' +import { CowSwapWidgetParams, TradeType } from '@cowprotocol/widget-lib' import { isDev, isLocalHost, isVercel } from '../../../env' import { ConfiguratorState } from '../types' @@ -31,6 +31,10 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi sellTokenAmount, buyToken, buyTokenAmount, + deadline, + swapDeadline, + limitDeadline, + advancedDeadline, tokenListUrls, customColors, defaultColors, @@ -57,6 +61,14 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi tradeType: currentTradeType, sell: { asset: sellToken, amount: sellTokenAmount ? sellTokenAmount.toString() : undefined }, buy: { asset: buyToken, amount: buyTokenAmount?.toString() }, + forcedOrderDeadline: + swapDeadline || limitDeadline || advancedDeadline + ? { + [TradeType.SWAP]: swapDeadline, + [TradeType.LIMIT]: limitDeadline, + [TradeType.ADVANCED]: advancedDeadline, + } + : deadline, enabledTradeTypes, theme: JSON.stringify(customColors) === JSON.stringify(defaultColors) diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx index 2bd0b12d5f..5aed4286e6 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/index.tsx @@ -34,6 +34,7 @@ import { CurrencyInputControl } from './controls/CurrencyInputControl' import { CurrentTradeTypeControl } from './controls/CurrentTradeTypeControl' import { CustomImagesControl } from './controls/CustomImagesControl' import { CustomSoundsControl } from './controls/CustomSoundsControl' +import { DeadlineControl } from './controls/DeadlineControl' import { NetworkControl, NetworkOption, NetworkOptions } from './controls/NetworkControl' import { PaletteControl } from './controls/PaletteControl' import { PartnerFeeControl } from './controls/PartnerFeeControl' @@ -106,6 +107,15 @@ export function Configurator({ title }: { title: string }) { const [buyToken] = buyTokenState const [buyTokenAmount] = buyTokenAmountState + const deadlineState = useState() + const [deadline] = deadlineState + const swapDeadlineState = useState() + const [swapDeadline] = swapDeadlineState + const limitDeadlineState = useState() + const [limitDeadline] = limitDeadlineState + const advancedDeadlineState = useState() + const [advancedDeadline] = advancedDeadlineState + const tokenListUrlsState = useState(DEFAULT_TOKEN_LISTS) const customTokensState = useState([]) const [tokenListUrls] = tokenListUrlsState @@ -146,6 +156,10 @@ export function Configurator({ title }: { title: string }) { // Don't change chainId in the widget URL if the user is connected to a wallet // Because useSyncWidgetNetwork() will send a request to change the network const state: ConfiguratorState = { + deadline, + swapDeadline, + limitDeadline, + advancedDeadline, chainId: IS_IFRAME ? undefined : !isConnected || !walletChainId ? chainId : walletChainId, theme: mode, currentTradeType, @@ -261,6 +275,16 @@ export function Configurator({ title }: { title: string }) { + Forced Order Deadline + + Global deadline settings + + + Individual deadline settings + + + + Integrations diff --git a/apps/widget-configurator/src/app/configurator/types.ts b/apps/widget-configurator/src/app/configurator/types.ts index 2755dbb135..d48f94df67 100644 --- a/apps/widget-configurator/src/app/configurator/types.ts +++ b/apps/widget-configurator/src/app/configurator/types.ts @@ -21,6 +21,10 @@ export interface ConfiguratorState { sellTokenAmount: number | undefined buyToken: string buyTokenAmount: number | undefined + deadline: number | undefined + swapDeadline: number | undefined + limitDeadline: number | undefined + advancedDeadline: number | undefined tokenListUrls: TokenListItem[] customColors: ColorPalette defaultColors: ColorPalette diff --git a/libs/common-const/src/misc.ts b/libs/common-const/src/misc.ts index 3fd037eb9f..2e8bed590c 100644 --- a/libs/common-const/src/misc.ts +++ b/libs/common-const/src/misc.ts @@ -6,7 +6,6 @@ export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' // 30 minutes, denominated in seconds export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30 -export const L2_DEADLINE_FROM_NOW = 60 * 5 // one basis JSBI.BigInt const BPS_BASE = JSBI.BigInt(10000) diff --git a/libs/widget-lib/src/types.ts b/libs/widget-lib/src/types.ts index 368ef01970..4f94816c43 100644 --- a/libs/widget-lib/src/types.ts +++ b/libs/widget-lib/src/types.ts @@ -1,5 +1,6 @@ import type { SupportedChainId } from '@cowprotocol/cow-sdk' import { CowWidgetEventListeners, CowWidgetEventPayloadMap, CowWidgetEvents } from '@cowprotocol/events' + export type { SupportedChainId } from '@cowprotocol/cow-sdk' export type PerTradeTypeConfig = Partial> @@ -82,6 +83,8 @@ interface TradeAsset { amount?: string } +export type ForcedOrderDeadline = FlexibleConfig + export enum TradeType { SWAP = 'swap', LIMIT = 'limit', @@ -230,6 +233,15 @@ export interface CowSwapWidgetParams { */ buy?: TradeAsset + /** + * Forced order deadline in minutes. When set, user's won't be able to edit the deadline. + * + * Either a single value applied to each individual order type accordingly or an optional individual value per order type. + * + * The app will use the appropriated min/max value per order type. + */ + forcedOrderDeadline?: ForcedOrderDeadline + /** * Enables the ability to switch between trade types in the widget. */