Skip to content

Commit

Permalink
feat(widget): deadline widget param (#4991)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alfetopito authored Oct 17, 2024
1 parent 0f53ad2 commit ce3b5b8
Show file tree
Hide file tree
Showing 19 changed files with 324 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useContext, useRef, useState } from 'react'
import { useCallback, useContext, useEffect, useRef, useState } from 'react'

import {
DEFAULT_DEADLINE_FROM_NOW,
Expand All @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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<SlippageError | false>(false)
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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(() => {
Expand Down Expand Up @@ -417,6 +439,7 @@ export function TransactionSettings() {
setDeadlineError(false)
}}
color={deadlineError ? 'red' : ''}
disabled={isDeadlineDisabled}
/>
</OptionCustom>
<ThemedText.Body style={{ paddingLeft: '8px' }} fontSize={14}>
Expand Down
17 changes: 8 additions & 9 deletions apps/cowswap-frontend/src/legacy/state/user/hooks.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()
Expand All @@ -48,7 +49,7 @@ export function useUserLocaleManager(): [SupportedLocale | null, (newLocale: Sup
(newLocale: SupportedLocale) => {
dispatch(updateUserLocale({ userLocale: newLocale }))
},
[dispatch]
[dispatch],
)

return [locale, setLocale]
Expand All @@ -67,7 +68,7 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void
(recipient: string | null) => {
dispatch(setRecipient({ recipient }))
},
[dispatch]
[dispatch],
)

const toggleVisibility = useCallback(
Expand All @@ -78,23 +79,21 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void
onChangeRecipient(null)
}
},
[isVisible, dispatch, onChangeRecipient]
[isVisible, dispatch, onChangeRecipient],
)

return [isVisible, toggleVisibility]
}

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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions apps/cowswap-frontend/src/modules/injectedWidget/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,29 +19,52 @@ export function DeadlineInput() {
const { deadlineMilliseconds, customDeadlineTimestamp } = useAtomValue(limitOrdersSettingsAtom)
const updateSettingsState = useSetAtom(updateLimitOrdersSettingsAtom)
const currentDeadlineNode = useRef<HTMLButtonElement>()
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 (
<DeadlineSelector
deadline={existingDeadline}
customDeadline={customDeadlineTimestamp}
isDeadlineDisabled={isDeadlineDisabled}
selectDeadline={selectDeadline}
selectCustomDeadline={selectCustomDeadline}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ms from 'ms.macro'
import { format } from 'timeago.js'

import { MAX_ORDER_DEADLINE } from 'common/constants/common'

Expand All @@ -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` },
Expand All @@ -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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const Fixtures = {
customDeadline={null}
selectDeadline={() => void 0}
selectCustomDeadline={() => void 0}
isDeadlineDisabled={false}
/>
),
}
Expand Down
Loading

0 comments on commit ce3b5b8

Please sign in to comment.