Skip to content

Commit

Permalink
Merge pull request #5000 from cowprotocol/release/17-10-2024
Browse files Browse the repository at this point in the history
Release 17-10-2024
  • Loading branch information
alfetopito authored Oct 18, 2024
2 parents d6c25b8 + c6ea5af commit 4c885e4
Show file tree
Hide file tree
Showing 58 changed files with 1,075 additions and 474 deletions.
9 changes: 8 additions & 1 deletion apps/cowswap-frontend/src/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ export const Routes = {
export type RoutesKeys = keyof typeof Routes
export type RoutesValues = (typeof Routes)[RoutesKeys]

export const MENU_ITEMS: { route: RoutesValues; label: string; fullLabel?: string; description: string }[] = [
export const MENU_ITEMS: {
route: RoutesValues
label: string
fullLabel?: string
description: string
badge?: string
}[] = [
{ route: Routes.SWAP, label: 'Swap', description: 'Trade tokens' },
{ route: Routes.LIMIT_ORDER, label: 'Limit', fullLabel: 'Limit order', description: 'Set your own price' },
{ route: Routes.ADVANCED_ORDERS, label: 'TWAP', description: 'Place orders with a time-weighted average price' },
Expand All @@ -47,4 +53,5 @@ export const HOOKS_STORE_MENU_ITEM = {
route: Routes.HOOKS,
label: 'Hooks',
description: 'Powerful tool to generate pre/post interaction for CoW Protocol',
badge: 'New',
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import ms from 'ms.macro'

import { useRefetchQuoteCallback } from 'legacy/hooks/useRefetchPriceCallback'
import { useAllQuotes, useIsBestQuoteLoading, useSetQuoteError } from 'legacy/state/price/hooks'
import { QuoteInformationObject } from 'legacy/state/price/reducer'
import { LegacyFeeQuoteParams } from 'legacy/state/price/types'
import { isWrappingTrade } from 'legacy/state/swap/utils'
import { Field } from 'legacy/state/types'
import { useUserTransactionTTL } from 'legacy/state/user/hooks'
Expand All @@ -22,95 +20,11 @@ import { useAppData } from 'modules/appData'
import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow'
import { useDerivedSwapInfo, useSwapState } from 'modules/swap/hooks/useSwapState'

import { isRefetchQuoteRequired } from './isRefetchQuoteRequired'
import { quoteUsingSameParameters } from './quoteUsingSameParameters'

export const TYPED_VALUE_DEBOUNCE_TIME = 350
export const SWAP_QUOTE_CHECK_INTERVAL = ms`30s` // Every 30s
const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = ms`30s` // Will renew the quote if there's less than 30 seconds left for the quote to expire
const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = ms`5s` // Prevents from sending the same request to often (max, every 5s)

type FeeQuoteParams = Omit<LegacyFeeQuoteParams, 'validTo'>

/**
* Returns if the quote has been recently checked
*/
function wasQuoteCheckedRecently(lastQuoteCheck: number): boolean {
return lastQuoteCheck + WAITING_TIME_BETWEEN_EQUAL_REQUESTS > Date.now()
}

/**
* Returns true if the fee quote expires soon (in less than RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME milliseconds)
*/
function isExpiringSoon(quoteExpirationIsoDate: string, threshold: number): boolean {
const feeExpirationDate = Date.parse(quoteExpirationIsoDate)
return feeExpirationDate <= Date.now() + threshold
}

/**
* Checks if the parameters for the current quote are correct
*
* Quotes are only valid for a given token-pair and amount. If any of these parameter change, the fee needs to be re-fetched
*/
function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: QuoteInformationObject): boolean {
const {
amount: currentAmount,
sellToken: currentSellToken,
buyToken: currentBuyToken,
kind: currentKind,
userAddress: currentUserAddress,
receiver: currentReceiver,
appData: currentAppData,
} = currentParams
const { amount, buyToken, sellToken, kind, userAddress, receiver, appData } = quoteInfo
const hasSameReceiver = currentReceiver && receiver ? currentReceiver === receiver : true

// cache the base quote params without quoteInfo user address to check
const paramsWithoutAddress =
sellToken === currentSellToken &&
buyToken === currentBuyToken &&
amount === currentAmount &&
kind === currentKind &&
appData === currentAppData &&
hasSameReceiver
// 2 checks: if there's a quoteInfo user address (meaning quote was already calculated once) and one without
// in case user is not connected
return userAddress ? currentUserAddress === userAddress && paramsWithoutAddress : paramsWithoutAddress
}

/**
* Decides if we need to refetch the fee information given the current parameters (selected by the user), and the current feeInfo (in the state)
*/
function isRefetchQuoteRequired(
isLoading: boolean,
currentParams: FeeQuoteParams,
quoteInformation?: QuoteInformationObject
): boolean {
// If there's no quote/fee information, we always re-fetch
if (!quoteInformation) {
return true
}

if (!quoteUsingSameParameters(currentParams, quoteInformation)) {
// If the current parameters don't match the fee, the fee information is invalid and needs to be re-fetched
return true
}

// The query params are the same, so we only ask for a new quote if:
// - If the quote was not queried recently
// - There's not another price query going on right now
// - The quote will expire soon
if (wasQuoteCheckedRecently(quoteInformation.lastCheck)) {
// Don't Re-fetch if it was queried recently
return false
} else if (isLoading) {
// Don't Re-fetch if there's another quote going on with the same params
// It's better to wait for the timeout or resolution. Also prevents an issue of refreshing too fast with slow APIs
return false
} else if (quoteInformation.fee) {
// Re-fetch if the fee is expiring soon
return isExpiringSoon(quoteInformation.fee.expirationDate, RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME)
}

return false
}

export function FeesUpdater(): null {
const { chainId, account } = useWalletInfo()
Expand Down Expand Up @@ -219,7 +133,7 @@ export function FeesUpdater(): null {
// Callback to re-fetch both the fee and the price
const refetchQuoteIfRequired = () => {
// if no token is unsupported and needs refetching
const hasToRefetch = !unsupportedToken && isRefetchQuoteRequired(isLoading, quoteParams, quoteInfo)
const hasToRefetch = !unsupportedToken && isRefetchQuoteRequired(isLoading, quoteParams, quoteInfo) //

if (hasToRefetch) {
// Decide if this is a new quote, or just a refresh
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import ms from 'ms.macro'

import { QuoteInformationObject } from 'legacy/state/price/reducer'
import { LegacyFeeQuoteParams } from 'legacy/state/price/types'

import { quoteUsingSameParameters } from './quoteUsingSameParameters'

const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = ms`30s` // Will renew the quote if there's less than 30 seconds left for the quote to expire
const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = ms`5s` // Prevents from sending the same request to often (max, every 5s)

type FeeQuoteParams = Omit<LegacyFeeQuoteParams, 'validTo'>

/**
* Returns if the quote has been recently checked
*/
function wasQuoteCheckedRecently(lastQuoteCheck: number): boolean {
return lastQuoteCheck + WAITING_TIME_BETWEEN_EQUAL_REQUESTS > Date.now()
}

/**
* Returns true if the fee quote expires soon (in less than RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME milliseconds)
*/
function isExpiringSoon(quoteExpirationIsoDate: string, threshold: number): boolean {
const feeExpirationDate = Date.parse(quoteExpirationIsoDate)
return feeExpirationDate <= Date.now() + threshold
}

/**
* Decides if we need to refetch the fee information given the current parameters (selected by the user), and the current feeInfo (in the state)
*/
export function isRefetchQuoteRequired(
isLoading: boolean,
currentParams: FeeQuoteParams,
quoteInformation?: QuoteInformationObject,
): boolean {
// If there's no quote/fee information, we always re-fetch
if (!quoteInformation) {
return true
}

if (!quoteUsingSameParameters(currentParams, quoteInformation)) {
// If the current parameters don't match the fee, the fee information is invalid and needs to be re-fetched
return true
}

// The query params are the same, so we only ask for a new quote if:
// - If the quote was not queried recently
// - There's not another price query going on right now
// - The quote will expire soon
if (wasQuoteCheckedRecently(quoteInformation.lastCheck)) {
// Don't Re-fetch if it was queried recently
return false
} else if (isLoading) {
// Don't Re-fetch if there's another quote going on with the same params
// It's better to wait for the timeout or resolution. Also prevents an issue of refreshing too fast with slow APIs
return false
} else if (quoteInformation.fee) {
// Re-fetch if the fee is expiring soon
return isExpiringSoon(quoteInformation.fee.expirationDate, RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME)
}

return false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { QuoteInformationObject } from 'legacy/state/price/reducer'
import { LegacyFeeQuoteParams } from 'legacy/state/price/types'

import { decodeAppData } from 'modules/appData'

type FeeQuoteParams = Omit<LegacyFeeQuoteParams, 'validTo'>

/**
* Checks if the parameters for the current quote are correct
*
* Quotes are only valid for a given token-pair and amount. If any of these parameter change, the fee needs to be re-fetched
*/
export function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: QuoteInformationObject): boolean {
const {
amount: currentAmount,
sellToken: currentSellToken,
buyToken: currentBuyToken,
kind: currentKind,
userAddress: currentUserAddress,
receiver: currentReceiver,
appData: currentAppData,
} = currentParams
const { amount, buyToken, sellToken, kind, userAddress, receiver, appData } = quoteInfo
const hasSameReceiver = currentReceiver && receiver ? currentReceiver === receiver : true
const hasSameAppData = compareAppDataWithoutQuoteData(appData, currentAppData)

// cache the base quote params without quoteInfo user address to check
const paramsWithoutAddress =
sellToken === currentSellToken &&
buyToken === currentBuyToken &&
amount === currentAmount &&
kind === currentKind &&
hasSameAppData &&
hasSameReceiver
// 2 checks: if there's a quoteInfo user address (meaning quote was already calculated once) and one without
// in case user is not connected
return userAddress ? currentUserAddress === userAddress && paramsWithoutAddress : paramsWithoutAddress
}

/**
* Compares appData without taking into account the `quote` metadata
*/
function compareAppDataWithoutQuoteData<T extends string | undefined>(a: T, b: T): boolean {
if (a === b) {
return true
}
const cleanedA = removeQuoteMetadata(a)
const cleanedB = removeQuoteMetadata(b)

return cleanedA === cleanedB
}

/**
* If appData is set and is valid, remove `quote` metadata from it
*/
function removeQuoteMetadata(appData: string | undefined): string | undefined {
if (!appData) {
return
}

const decoded = decodeAppData(appData)

if (!decoded) {
return
}

const { metadata: fullMetadata, ...rest } = decoded
const { quote: _, ...metadata } = fullMetadata
return JSON.stringify({ ...rest, metadata })
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import styled from 'styled-components/macro'

import { useDarkModeManager } from 'legacy/state/user/hooks'

import { useInjectedWidgetParams } from 'modules/injectedWidget'

const HideSmall = styled.span`
${Media.upToSmall()} {
display: none;
Expand Down Expand Up @@ -61,7 +63,9 @@ const StyledArrowUpRight = styled(ArrowUpRight)`

const ContentWrapper = styled.div<{ chainId: NetworkAlertChains; darkMode: boolean; logoUrl: string }>`
background: var(${UI.COLOR_PAPER_DARKER});
transition: color var(${UI.ANIMATION_DURATION}) ease-in-out, background var(${UI.ANIMATION_DURATION}) ease-in-out; // MOD
transition:
color var(${UI.ANIMATION_DURATION}) ease-in-out,
background var(${UI.ANIMATION_DURATION}) ease-in-out; // MOD
border-radius: 20px;
display: flex;
flex-direction: row;
Expand All @@ -88,7 +92,9 @@ const ContentWrapper = styled.div<{ chainId: NetworkAlertChains; darkMode: boole
color: inherit;
stroke: currentColor;
text-decoration: none;
transition: transform var(${UI.ANIMATION_DURATION}) ease-in-out, stroke var(${UI.ANIMATION_DURATION}) ease-in-out,
transition:
transform var(${UI.ANIMATION_DURATION}) ease-in-out,
stroke var(${UI.ANIMATION_DURATION}) ease-in-out,
color var(${UI.ANIMATION_DURATION}) ease-in-out;
}
Expand Down Expand Up @@ -136,7 +142,9 @@ export function NetworkAlert() {

const theme = useTheme()

if (!shouldShowAlert(chainId) || !isActive) {
const { hideBridgeInfo } = useInjectedWidgetParams()

if (!shouldShowAlert(chainId) || !isActive || hideBridgeInfo) {
return null
}

Expand Down
34 changes: 32 additions & 2 deletions apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { Command } from '@cowprotocol/types'
import { HoverTooltip } from '@cowprotocol/ui'
import { Fraction } from '@uniswap/sdk-core'
Expand Down Expand Up @@ -45,7 +44,7 @@ const WarningCheckboxContainer = styled.span`
const WarningContainer = styled(AuxInformationContainer).attrs((props) => ({
...props,
hideInput: true,
})) <HighFeeContainerProps>`
}))<HighFeeContainerProps>`
--warningColor: ${({ theme, level }) =>
level === HIGH_TIER_FEE
? theme.danger
Expand Down Expand Up @@ -171,3 +170,34 @@ export const HighFeeWarning = (props: WarningProps) => {
</WarningContainer>
)
}

export type HighSuggestedSlippageWarningProps = {
isSuggestedSlippage: boolean | undefined
slippageBps: number | undefined
className?: string
} & HighFeeContainerProps

export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarningProps) {
const { isSuggestedSlippage, slippageBps, ...rest } = props

if (!isSuggestedSlippage || !slippageBps || slippageBps <= 200) {
return null
}

return (
<WarningContainer {...rest} level={LOW_TIER_FEE}>
<div>
<AlertTriangle size={24} />
Slippage adjusted to {`${slippageBps / 100}`}% to ensure quick execution
<HoverTooltip
wrapInContainer
content={
'CoW Swap dynamically adjusts your slippage tolerance based on current volatility. You can set a custom slippage using the settings icon above.'
}
>
<ErrorStyledInfoIcon />
</HoverTooltip>
</div>
</WarningContainer>
)
}
Loading

0 comments on commit 4c885e4

Please sign in to comment.