From c6ea5af5d24b9a806540d53d2a0d9e12799d4eff Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 17 Oct 2024 16:42:54 +0100 Subject: [PATCH 1/9] fix(smart-slippage): fix smart slip tooltip and feature flag (#5004) * fix: remove hard coded smart slippage value * fix: handle case when multiplier percentage in falsy * feat: use different tooltip when feature flag is off * fix: pass dynamic slippage settings to row slippage content * chore: fix lint --- .../components/TransactionSettings/index.tsx | 2 +- .../ConfirmSwapModalSetup/index.tsx | 4 ++- .../pure/Row/RowSlippageContent/index.tsx | 34 +++++++++++++------ .../calculateBpsFromFeeMultiplier.ts | 2 +- .../useSmartSlippageFromFeeMultiplier.ts | 3 +- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx index 7ad9ea8175..371ac7abbb 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx @@ -335,7 +335,7 @@ export function TransactionSettings() { // Your transaction will revert if the price changes unfavorably by more than this percentage. isEoaEthFlow ? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol, getWrappedToken(nativeCurrency).symbol]) - : getNonNativeSlippageTooltip(true) + : getNonNativeSlippageTooltip({ isDynamic: !!smartSlippage, isSettingsModal: true }) } /> diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index 9b58d26442..e904e03fa6 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -35,6 +35,7 @@ import { useIsEoaEthFlow } from '../../hooks/useIsEoaEthFlow' import { useNavigateToNewOrderCallback } from '../../hooks/useNavigateToNewOrderCallback' import { useShouldPayGas } from '../../hooks/useShouldPayGas' import { useSwapConfirmButtonText } from '../../hooks/useSwapConfirmButtonText' +import { useSmartSwapSlippage } from '../../hooks/useSwapSlippage' import { useSwapState } from '../../hooks/useSwapState' import { NetworkCostsTooltipSuffix } from '../../pure/NetworkCostsTooltipSuffix' import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from '../../pure/Row/RowSlippageContent' @@ -87,6 +88,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const buttonText = useSwapConfirmButtonText(slippageAdjustedSellAmount) const isSmartSlippageApplied = useIsSmartSlippageApplied() + const smartSlippage = useSmartSwapSlippage() const labelsAndTooltips = useMemo( () => ({ @@ -96,7 +98,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { : undefined, slippageTooltip: isEoaEthFlow ? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol]) - : getNonNativeSlippageTooltip(), + : getNonNativeSlippageTooltip({ isDynamic: !!smartSlippage }), expectReceiveLabel: isExactIn ? 'Expected to receive' : 'Expected to sell', minReceivedLabel: isExactIn ? 'Minimum receive' : 'Maximum sent', minReceivedTooltip: getMinimumReceivedTooltip(allowedSlippage, isExactIn), diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx index 07a343a2fd..4facd945c0 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx @@ -53,19 +53,30 @@ export const getNativeSlippageTooltip = (chainId: SupportedChainId, symbols: (st robust MEV protection, consider wrapping your {symbols?.[0] || 'native currency'} before trading. ) -export const getNonNativeSlippageTooltip = (isSettingsModal?: boolean) => ( + +export const getNonNativeSlippageTooltip = (params?: { isDynamic?: boolean; isSettingsModal?: boolean }) => ( - CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting the - best price.{' '} - {isSettingsModal ? ( + {params?.isDynamic ? ( <> - To override this, enter your desired slippage amount. -
-
- Either way, your slippage is protected from MEV! + CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting + the best price.{' '} + {params?.isSettingsModal ? ( + <> + To override this, enter your desired slippage amount. +
+
+ Either way, your slippage is protected from MEV! + + ) : ( + <> +
+
+ Trades are protected from MEV, so your slippage can't be exploited! + + )} ) : ( - "Trades are protected from MEV, so your slippage can't be exploited!" + <>CoW Swap trades are protected from MEV, so your slippage can't be exploited! )}
) @@ -113,7 +124,10 @@ export function RowSlippageContent(props: RowSlippageContentProps) { } = props const tooltipContent = - slippageTooltip || (isEoaEthFlow ? getNativeSlippageTooltip(chainId, symbols) : getNonNativeSlippageTooltip()) + slippageTooltip || + (isEoaEthFlow + ? getNativeSlippageTooltip(chainId, symbols) + : getNonNativeSlippageTooltip({ isDynamic: !!smartSlippage })) // In case the user happened to set the same slippage as the suggestion, do not show the suggestion const suggestedEqualToUserSlippage = smartSlippage && smartSlippage === displaySlippage diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts index 73d161d60d..fdd71e5f9f 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts @@ -8,7 +8,7 @@ export function calculateBpsFromFeeMultiplier( isSell: boolean | undefined, multiplierPercentage: number, ): number | undefined { - if (!sellAmount || !feeAmount || isSell === undefined || multiplierPercentage <= 0) { + if (!sellAmount || !feeAmount || isSell === undefined || !multiplierPercentage || multiplierPercentage <= 0) { return undefined } diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts index f5566085a1..d80af85a40 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts @@ -6,7 +6,6 @@ import { useReceiveAmountInfo } from 'modules/trade' import { calculateBpsFromFeeMultiplier } from './calculateBpsFromFeeMultiplier' - /** * Calculates smart slippage in bps, based on quoted fee * @@ -19,7 +18,7 @@ export function useSmartSlippageFromFeeMultiplier(): number | undefined { const sellAmount = isSell ? afterNetworkCosts?.sellAmount : beforeNetworkCosts?.sellAmount const feeAmount = costs?.networkFee?.amountInSellCurrency - const { smartSlippageFeeMultiplierPercentage = 50 } = useFeatureFlags() + const { smartSlippageFeeMultiplierPercentage } = useFeatureFlags() return useMemo( () => calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, smartSlippageFeeMultiplierPercentage), From b47e3635b9b3f0f918c5d4268d2b3e33d5fc009b Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 15:31:26 +0500 Subject: [PATCH 2/9] chore: release main (#5007) --- .release-please-manifest.json | 14 +++++++------- apps/cowswap-frontend/CHANGELOG.md | 19 +++++++++++++++++++ apps/cowswap-frontend/package.json | 2 +- apps/explorer/CHANGELOG.md | 7 +++++++ apps/explorer/package.json | 2 +- apps/widget-configurator/CHANGELOG.md | 9 +++++++++ apps/widget-configurator/package.json | 2 +- libs/common-const/CHANGELOG.md | 8 ++++++++ libs/common-const/package.json | 2 +- libs/hook-dapp-lib/CHANGELOG.md | 8 ++++++++ libs/hook-dapp-lib/package.json | 2 +- libs/ui/CHANGELOG.md | 7 +++++++ libs/ui/package.json | 2 +- libs/widget-lib/CHANGELOG.md | 9 +++++++++ libs/widget-lib/package.json | 2 +- 15 files changed, 81 insertions(+), 14 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 338c004e91..b52eed9497 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,13 +1,13 @@ { - "apps/cowswap-frontend": "1.85.0", - "apps/explorer": "2.35.1", + "apps/cowswap-frontend": "1.86.0", + "apps/explorer": "2.35.2", "libs/permit-utils": "0.4.0", - "libs/widget-lib": "0.15.0", + "libs/widget-lib": "0.16.0", "libs/widget-react": "0.11.0", - "apps/widget-configurator": "1.7.2", + "apps/widget-configurator": "1.8.0", "libs/analytics": "1.8.0", "libs/assets": "1.8.0", - "libs/common-const": "1.7.0", + "libs/common-const": "1.8.0", "libs/common-hooks": "1.4.0", "libs/common-utils": "1.7.2", "libs/core": "1.3.0", @@ -16,7 +16,7 @@ "libs/snackbars": "1.1.0", "libs/tokens": "1.10.0", "libs/types": "1.2.0", - "libs/ui": "1.10.1", + "libs/ui": "1.11.0", "libs/wallet": "1.6.0", "apps/cow-fi": "1.15.0", "libs/wallet-provider": "1.0.0", @@ -24,5 +24,5 @@ "libs/abis": "1.2.0", "libs/balances-and-allowances": "1.0.0", "libs/iframe-transport": "1.0.0", - "libs/hook-dapp-lib": "1.1.0" + "libs/hook-dapp-lib": "1.2.0" } diff --git a/apps/cowswap-frontend/CHANGELOG.md b/apps/cowswap-frontend/CHANGELOG.md index ad2177ef72..560739bd2d 100644 --- a/apps/cowswap-frontend/CHANGELOG.md +++ b/apps/cowswap-frontend/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [1.86.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.85.0...cowswap-v1.86.0) (2024-10-18) + + +### Features + +* display new label for cow amm ([#4994](https://github.com/cowprotocol/cowswap/issues/4994)) ([531e63f](https://github.com/cowprotocol/cowswap/commit/531e63f666ffcafdaf8e2b1c2850991facbe5cf1)) +* **hooks-store:** add claim vesting iframe hook ([#4924](https://github.com/cowprotocol/cowswap/issues/4924)) ([395f48f](https://github.com/cowprotocol/cowswap/commit/395f48f57d93de67305791fdb9a668bdd693074e)) +* **hooks-store:** add sell/buy amounts to hook-dapp context ([#4990](https://github.com/cowprotocol/cowswap/issues/4990)) ([26cbffb](https://github.com/cowprotocol/cowswap/commit/26cbffbbfe8edbc0a4a9ba31fe9c0d42852118d9)) +* **slippage:** small order slippage v2 ([#4934](https://github.com/cowprotocol/cowswap/issues/4934)) ([7b2a49c](https://github.com/cowprotocol/cowswap/commit/7b2a49c41ecfd62107a3128e771003743094d246)) +* **smart-slippage:** update smart slippage text ([#4982](https://github.com/cowprotocol/cowswap/issues/4982)) ([4b89ecb](https://github.com/cowprotocol/cowswap/commit/4b89ecbf661e6c30193586c704e23c78b2bfc22b)) +* **widget:** deadline widget param ([#4991](https://github.com/cowprotocol/cowswap/issues/4991)) ([ce3b5b8](https://github.com/cowprotocol/cowswap/commit/ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2)) +* **widget:** hide bridge info ([#4992](https://github.com/cowprotocol/cowswap/issues/4992)) ([9842afd](https://github.com/cowprotocol/cowswap/commit/9842afdb887497d235a01538663488b0b8852bb5)) +* **widget:** hide orders table ([#4993](https://github.com/cowprotocol/cowswap/issues/4993)) ([681fb20](https://github.com/cowprotocol/cowswap/commit/681fb20dab0b4155d50ad7f32c7a48cb95e084a3)) + + +### Bug Fixes + +* **smart-slippage:** fix smart slip tooltip and feature flag ([#5004](https://github.com/cowprotocol/cowswap/issues/5004)) ([c6ea5af](https://github.com/cowprotocol/cowswap/commit/c6ea5af5d24b9a806540d53d2a0d9e12799d4eff)) + ## [1.85.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.84.0...cowswap-v1.85.0) (2024-10-10) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index e3d977b4f7..a5898d6e9b 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cowswap", - "version": "1.85.0", + "version": "1.86.0", "description": "CoW Swap", "main": "index.js", "author": "", diff --git a/apps/explorer/CHANGELOG.md b/apps/explorer/CHANGELOG.md index 3c9f1687c5..38a0d41e0f 100644 --- a/apps/explorer/CHANGELOG.md +++ b/apps/explorer/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.35.2](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.1...explorer-v2.35.2) (2024-10-18) + + +### Bug Fixes + +* **explorer:** display hook details of unknown hook-dapp ([#4995](https://github.com/cowprotocol/cowswap/issues/4995)) ([eaa29f3](https://github.com/cowprotocol/cowswap/commit/eaa29f3ed421d92214b857bf1c57d75b0317cbba)) + ## [2.35.1](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.0...explorer-v2.35.1) (2024-10-14) diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 34f4e6a3e7..541ed15881 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/explorer", - "version": "2.35.1", + "version": "2.35.2", "description": "CoW Swap Explorer", "main": "src/main.tsx", "author": "", diff --git a/apps/widget-configurator/CHANGELOG.md b/apps/widget-configurator/CHANGELOG.md index 179cf695e8..06b0a94e05 100644 --- a/apps/widget-configurator/CHANGELOG.md +++ b/apps/widget-configurator/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.8.0](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.7.2...widget-configurator-v1.8.0) (2024-10-18) + + +### Features + +* **widget:** deadline widget param ([#4991](https://github.com/cowprotocol/cowswap/issues/4991)) ([ce3b5b8](https://github.com/cowprotocol/cowswap/commit/ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2)) +* **widget:** hide bridge info ([#4992](https://github.com/cowprotocol/cowswap/issues/4992)) ([9842afd](https://github.com/cowprotocol/cowswap/commit/9842afdb887497d235a01538663488b0b8852bb5)) +* **widget:** hide orders table ([#4993](https://github.com/cowprotocol/cowswap/issues/4993)) ([681fb20](https://github.com/cowprotocol/cowswap/commit/681fb20dab0b4155d50ad7f32c7a48cb95e084a3)) + ## [1.7.2](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.7.1...widget-configurator-v1.7.2) (2024-10-08) diff --git a/apps/widget-configurator/package.json b/apps/widget-configurator/package.json index b903661cb1..8efbe145c2 100644 --- a/apps/widget-configurator/package.json +++ b/apps/widget-configurator/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-configurator", - "version": "1.7.2", + "version": "1.8.0", "description": "CoW Swap widget configurator", "main": "src/main.tsx", "author": "", diff --git a/libs/common-const/CHANGELOG.md b/libs/common-const/CHANGELOG.md index a4af2ddeaf..f47c4b779f 100644 --- a/libs/common-const/CHANGELOG.md +++ b/libs/common-const/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.8.0](https://github.com/cowprotocol/cowswap/compare/common-const-v1.7.0...common-const-v1.8.0) (2024-10-18) + + +### Features + +* **smart-slippage:** update smart slippage text ([#4982](https://github.com/cowprotocol/cowswap/issues/4982)) ([4b89ecb](https://github.com/cowprotocol/cowswap/commit/4b89ecbf661e6c30193586c704e23c78b2bfc22b)) +* **widget:** deadline widget param ([#4991](https://github.com/cowprotocol/cowswap/issues/4991)) ([ce3b5b8](https://github.com/cowprotocol/cowswap/commit/ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2)) + ## [1.7.0](https://github.com/cowprotocol/cowswap/compare/common-const-v1.6.2...common-const-v1.7.0) (2024-07-12) diff --git a/libs/common-const/package.json b/libs/common-const/package.json index 72cb582543..b6222fc68f 100644 --- a/libs/common-const/package.json +++ b/libs/common-const/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/common-const", - "version": "1.7.0", + "version": "1.8.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/hook-dapp-lib/CHANGELOG.md b/libs/hook-dapp-lib/CHANGELOG.md index 92bc14bcf4..391264f286 100644 --- a/libs/hook-dapp-lib/CHANGELOG.md +++ b/libs/hook-dapp-lib/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.2.0](https://github.com/cowprotocol/cowswap/compare/hook-dapp-lib-v1.1.0...hook-dapp-lib-v1.2.0) (2024-10-18) + + +### Features + +* **hooks-store:** add claim vesting iframe hook ([#4924](https://github.com/cowprotocol/cowswap/issues/4924)) ([395f48f](https://github.com/cowprotocol/cowswap/commit/395f48f57d93de67305791fdb9a668bdd693074e)) +* **hooks-store:** add sell/buy amounts to hook-dapp context ([#4990](https://github.com/cowprotocol/cowswap/issues/4990)) ([26cbffb](https://github.com/cowprotocol/cowswap/commit/26cbffbbfe8edbc0a4a9ba31fe9c0d42852118d9)) + ## [1.1.0](https://github.com/cowprotocol/cowswap/compare/hook-dapp-lib-v1.0.0...hook-dapp-lib-v1.1.0) (2024-10-10) diff --git a/libs/hook-dapp-lib/package.json b/libs/hook-dapp-lib/package.json index f55cdc77df..f9dc713435 100644 --- a/libs/hook-dapp-lib/package.json +++ b/libs/hook-dapp-lib/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/hook-dapp-lib", - "version": "1.1.0", + "version": "1.2.0", "type": "commonjs", "description": "CoW Swap Hook Dapp Library. Allows you to develop pre/post hooks dapps for CoW Protocol.", "main": "index.js", diff --git a/libs/ui/CHANGELOG.md b/libs/ui/CHANGELOG.md index 4d9fbe85eb..1e00acf735 100644 --- a/libs/ui/CHANGELOG.md +++ b/libs/ui/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.11.0](https://github.com/cowprotocol/cowswap/compare/ui-v1.10.1...ui-v1.11.0) (2024-10-18) + + +### Features + +* display new label for cow amm ([#4994](https://github.com/cowprotocol/cowswap/issues/4994)) ([531e63f](https://github.com/cowprotocol/cowswap/commit/531e63f666ffcafdaf8e2b1c2850991facbe5cf1)) + ## [1.10.1](https://github.com/cowprotocol/cowswap/compare/ui-v1.10.0...ui-v1.10.1) (2024-10-10) diff --git a/libs/ui/package.json b/libs/ui/package.json index 7633ab1d29..ac6448fab6 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/ui", - "version": "1.10.1", + "version": "1.11.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/widget-lib/CHANGELOG.md b/libs/widget-lib/CHANGELOG.md index 695fc25fb7..4b9aa025fe 100644 --- a/libs/widget-lib/CHANGELOG.md +++ b/libs/widget-lib/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.16.0](https://github.com/cowprotocol/cowswap/compare/widget-lib-v0.15.0...widget-lib-v0.16.0) (2024-10-18) + + +### Features + +* **widget:** deadline widget param ([#4991](https://github.com/cowprotocol/cowswap/issues/4991)) ([ce3b5b8](https://github.com/cowprotocol/cowswap/commit/ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2)) +* **widget:** hide bridge info ([#4992](https://github.com/cowprotocol/cowswap/issues/4992)) ([9842afd](https://github.com/cowprotocol/cowswap/commit/9842afdb887497d235a01538663488b0b8852bb5)) +* **widget:** hide orders table ([#4993](https://github.com/cowprotocol/cowswap/issues/4993)) ([681fb20](https://github.com/cowprotocol/cowswap/commit/681fb20dab0b4155d50ad7f32c7a48cb95e084a3)) + ## [0.15.0](https://github.com/cowprotocol/cowswap/compare/widget-lib-v0.14.1...widget-lib-v0.15.0) (2024-09-30) diff --git a/libs/widget-lib/package.json b/libs/widget-lib/package.json index b2dcdaa138..acfa1891f2 100644 --- a/libs/widget-lib/package.json +++ b/libs/widget-lib/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-lib", - "version": "0.15.0", + "version": "0.16.0", "type": "commonjs", "description": "CoW Swap Widget Library. Allows you to easily embed a CoW Swap widget on your website.", "main": "index.js", From 02855f1fcaee7f74ff927848ed54e1913dc25cb9 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 16:14:30 +0500 Subject: [PATCH 3/9] chore: bump widget-react version (#5010) --- libs/widget-react/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/widget-react/package.json b/libs/widget-react/package.json index ede317c850..a903d60c04 100644 --- a/libs/widget-react/package.json +++ b/libs/widget-react/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-react", - "version": "0.11.0", + "version": "0.12.0", "type": "commonjs", "description": "CoW Swap Widget Library. Allows you to easily embed a CoW Swap widget on your React application.", "main": "index.js", @@ -22,6 +22,6 @@ "react" ], "dependencies": { - "@cowprotocol/widget-lib": "^0.15.0" + "@cowprotocol/widget-lib": "^0.16.0" } } From 3f8446b48a4f493448b262959b943756a24382d9 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 17:03:08 +0500 Subject: [PATCH 4/9] fix(widget): ignore selected eip6963 provider when in widget (#5009) --- apps/cowswap-frontend/src/cow-react/index.tsx | 7 ++++++- .../src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts | 6 ++++-- libs/wallet/src/web3-react/Web3Provider/index.tsx | 7 ++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/cowswap-frontend/src/cow-react/index.tsx b/apps/cowswap-frontend/src/cow-react/index.tsx index c525fe3bc8..bea80941b9 100644 --- a/apps/cowswap-frontend/src/cow-react/index.tsx +++ b/apps/cowswap-frontend/src/cow-react/index.tsx @@ -70,8 +70,13 @@ function Main() { function Web3ProviderInstance({ children }: { children: ReactNode }) { const selectedWallet = useAppSelector((state) => state.user.selectedWallet) + const { standaloneMode } = useInjectedWidgetParams() - return {children} + return ( + + {children} + + ) } function Toasts() { diff --git a/libs/wallet/src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts b/libs/wallet/src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts index a9a86fe0a2..22ac501bef 100644 --- a/libs/wallet/src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts +++ b/libs/wallet/src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts @@ -27,7 +27,7 @@ async function connect(connector: Connector) { } } -export function useEagerlyConnect(selectedWallet: ConnectionType | undefined) { +export function useEagerlyConnect(selectedWallet: ConnectionType | undefined, standaloneMode?: boolean) { const [tryConnectEip6963Provider, setTryConnectEip6963Provider] = useState(false) const eagerlyConnectInitRef = useRef(false) const selectedEip6963ProviderInfo = useSelectedEip6963ProviderInfo() @@ -75,6 +75,8 @@ export function useEagerlyConnect(selectedWallet: ConnectionType | undefined) { * Activate the selected eip6963 provider */ useEffect(() => { + // Ignore remembered eip6963 provider if the app is in widget dapp mode + if (isInjectedWidget() && !standaloneMode) return if (!selectedWallet || !tryConnectEip6963Provider) return const connection = getWeb3ReactConnection(selectedWallet) @@ -90,5 +92,5 @@ export function useEagerlyConnect(selectedWallet: ConnectionType | undefined) { setTryConnectEip6963Provider(false) connect(connector) } - }, [selectedEip6963ProviderInfo, selectedWallet, setEip6963Provider, tryConnectEip6963Provider]) + }, [standaloneMode, selectedEip6963ProviderInfo, selectedWallet, setEip6963Provider, tryConnectEip6963Provider]) } diff --git a/libs/wallet/src/web3-react/Web3Provider/index.tsx b/libs/wallet/src/web3-react/Web3Provider/index.tsx index d25571b454..97cc1dc8df 100644 --- a/libs/wallet/src/web3-react/Web3Provider/index.tsx +++ b/libs/wallet/src/web3-react/Web3Provider/index.tsx @@ -13,10 +13,11 @@ import { Web3ReactConnection } from '../types' interface Web3ProviderProps { children: ReactNode selectedWallet: ConnectionType | undefined + standaloneMode?: boolean } -export function Web3Provider({ children, selectedWallet }: Web3ProviderProps) { - useEagerlyConnect(selectedWallet) +export function Web3Provider({ children, selectedWallet, standaloneMode }: Web3ProviderProps) { + useEagerlyConnect(selectedWallet, standaloneMode) const connections = useOrderedConnections(selectedWallet) const connectors: [Connector, Web3ReactHooks][] = connections @@ -25,7 +26,7 @@ export function Web3Provider({ children, selectedWallet }: Web3ProviderProps) { const key = useMemo( () => connections.map(({ type }: Web3ReactConnection) => getConnectionName(type)).join('-'), - [connections] + [connections], ) return ( From 99c4c42aec60a734a37926935be5dca6cd4cf11c Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 17:32:46 +0500 Subject: [PATCH 5/9] feat: setup vampire attack widget (#4950) * feat(yield): setup yield widget * fix(yield): display correct output amount * feat(trade-quote): support fast quote requests * feat(yield): display trade buttons * feat(yield): display confirm details * feat(yield): scope context to trade * feat(yield): do trade after confirmation * feat(yield): display order progress bar * refactor: move useIsEoaEthFlow to trade module * refactor: move hooks to tradeSlippage module * refactor: rename swapSlippage to tradeSlippage * feat(trade-slippage): split slippage state by trade type * refactor: unlink TransactionSettings from swap module * refactor: use reach modal in Settings component * refactor: move Settings component in trade module * feat(yield): add settings widget * feat(yield): use deadline value from settings * fix(trade-quote): skip fast quote if it slower than optimal * refactor: generalise TradeRateDetails from swap module * refactor: move TradeRateDetails into tradeWidgetAddons module * refactor: move SettingsTab into tradeWidgetAddons module * refactor(swap): generalise useHighFeeWarning() * refactor(swap): move hooks to trade module * refactor: move HighFeeWarning to trade widget addons * refactor: make HighFeeWarning independent * feat(yield): display trade warnings ZeroApprovalWarning and HighFeeWarning * refactor(trade): generalise NoImpactWarning * refactor(trade): generalise ZeroApprovalWarning * refactor(trade): generalise bundle tx banners * refactor: extract TradeWarnings * chore: fix yield form displaying * refactor(swap): generalise trade flow * feat(yield): support safe bundle swaps * chore: hide yield under ff * chore: remove lazy loading * chore: fix imports * fix: generalize smart slippage usage * fix: don't sync url params while navigating with yield * feat: open settings menu on slippage click * chore: update btn text * fix: make slippage settings through * chore: merge develop * chore: fix yield widget * chore: slippage label * fix: fix default trade state in menu * chore: fix lint * chore: add yield in widget conf * chore: merge develop * chore: fix hooks trade state * fix: fix smart slippage displaying * chore: center slippage banner --- .../src/common/constants/routes.ts | 8 + .../src/common/hooks/useGetMarketDimension.ts | 1 + .../src/common/hooks/useMenuItems.ts | 17 +- .../TransactionSubmittedContent/index.tsx | 7 +- .../src/common/updaters/FeesUpdater/index.ts | 2 +- .../updaters/orders/PendingOrdersUpdater.ts | 28 +-- .../common/utils/tradeSettingsTooltips.tsx | 61 ++++++ .../src/legacy/components/Settings/index.tsx | 164 -------------- .../legacy/components/SwapWarnings/index.tsx | 203 ------------------ .../src/legacy/components/swap/styleds.tsx | 100 +-------- .../src/legacy/hooks/usePriceImpact/types.ts | 14 -- .../legacy/hooks/useRefetchPriceCallback.tsx | 8 +- .../src/legacy/state/application/hooks.ts | 4 - .../src/legacy/state/application/reducer.ts | 4 +- .../src/legacy/state/user/hooks.tsx | 10 +- .../containers/AdvancedOrdersWidget/index.tsx | 11 +- .../src/modules/analytics/events.ts | 1 + .../appData/updater/AppDataInfoUpdater.ts | 1 + .../application/containers/App/RoutesApp.tsx | 10 +- .../containers/HookDappContainer/index.tsx | 1 + .../useRescueFundsFromProxy.ts | 2 +- .../containers/TenderlySimulate/index.tsx | 2 +- .../hooksStore/dapps/BuildHookApp/index.tsx | 2 +- .../dapps/ClaimGnoHookApp/index.tsx | 2 +- .../hooksStore/dapps/PermitHookApp/index.tsx | 2 +- .../modules/hooksStore/hooks/useAddHook.ts | 2 +- .../modules/hooksStore/hooks/useRemoveHook.ts | 2 +- .../hooks/useSetRecipientOverride.ts | 2 +- .../hooks/useSetupHooksStoreOrderParams.ts | 30 +-- .../hooksStore/hooks/useTenderlySimulate.ts | 2 +- .../CustomDappLoader/index.tsx | 2 +- .../updaters/iframeDappsManifestUpdater.tsx | 4 +- .../containers/LimitOrdersWarnings/index.tsx | 59 +---- .../containers/LimitOrdersWidget/index.tsx | 47 ++-- .../hooks/useLimitOrdersWarningsAccepted.ts | 17 +- .../limitOrders/services/tradeFlow/index.ts | 2 +- .../state/limitOrdersWarningsAtom.ts | 2 - .../pure/ReceiptModal/OrderTypeField.tsx | 1 + .../src/modules/permit/hooks/usePermitInfo.ts | 1 + .../ConfirmSwapModalSetup/index.tsx | 69 ++---- .../swap/containers/Row/RowDeadline/index.tsx | 37 ---- .../swap/containers/Row/RowSlippage/index.tsx | 62 ------ .../containers/SurplusModalSetup/index.tsx | 3 +- .../swap/containers/SwapUpdaters/index.tsx | 11 +- .../swap/containers/SwapWidget/index.tsx | 152 +++++-------- .../modules/swap/helpers/getEthFlowEnabled.ts | 3 - .../swap/helpers/getSwapButtonState.ts | 3 +- .../hooks/useBaseSafeBundleFlowContext.ts | 43 ---- .../modules/swap/hooks/useEthFlowContext.ts | 64 +++--- .../src/modules/swap/hooks/useFlowContext.ts | 167 -------------- .../modules/swap/hooks/useHandleSwap.test.tsx | 85 -------- .../src/modules/swap/hooks/useHandleSwap.ts | 63 ------ .../swap/hooks/useHandleSwapOrEthFlow.ts | 42 ++++ .../hooks/useSafeBundleApprovalFlowContext.ts | 35 --- .../swap/hooks/useSafeBundleEthFlowContext.ts | 27 --- .../src/modules/swap/hooks/useSetSlippage.ts | 7 - .../swap/hooks/useSwapButtonContext.ts | 27 +-- .../swap/hooks/useSwapConfirmButtonText.tsx | 4 +- .../modules/swap/hooks/useSwapFlowContext.ts | 55 +---- .../src/modules/swap/hooks/useSwapSlippage.ts | 18 -- .../src/modules/swap/hooks/useSwapState.tsx | 104 +-------- .../hooks/useTradeQuoteStateFromLegacy.ts | 3 +- .../src/modules/swap/index.ts | 2 +- .../swap/pure/ReceiveAmountInfo/index.tsx | 2 +- .../swap/pure/Row/RowDeadline/index.tsx | 91 -------- .../src/modules/swap/pure/Row/types.ts | 5 - .../src/modules/swap/pure/Row/typings.ts | 4 - .../swap/pure/SwapButtons/index.cosmos.tsx | 1 - .../modules/swap/pure/SwapButtons/index.tsx | 1 - .../modules/swap/pure/SwapButtons/styled.tsx | 2 +- .../src/modules/swap/pure/styled.tsx | 26 --- .../src/modules/swap/pure/warnings.tsx | 73 +------ .../modules/swap/services/ethFlow/index.ts | 28 ++- .../src/modules/swap/services/types.ts | 70 +----- .../updaters/EthFlowDeadlineUpdater.tsx | 2 +- .../swap/state/baseFlowContextSourceAtom.ts | 5 - .../modules/swap/state/useSwapDerivedState.ts | 4 +- .../src/modules/swap/types/flowContext.ts | 51 ----- .../swap/updaters/BaseFlowContextUpdater.tsx | 147 ------------- .../containers/NoImpactWarning/index.tsx | 73 +++++++ .../TradeBasicConfirmDetails/index.tsx | 10 +- .../TradeConfirmModal/index.cosmos.tsx | 2 +- .../trade/containers/TradeWarnings/index.tsx | 65 ++++++ .../TradeWidget/TradeWidgetForm.tsx | 29 ++- .../TradeWidget/TradeWidgetUpdaters.tsx | 4 + .../trade/containers/TradeWidget/index.tsx | 8 +- .../trade/containers/TradeWidget/types.ts | 4 +- .../containers/TradeWidgetLinks/index.tsx | 44 +++- .../useSetupTradeStateFromUrl.ts | 6 +- .../{swap => trade}/hooks/useIsEoaEthFlow.ts | 0 .../{swap => trade}/hooks/useIsSafeEthFlow.ts | 0 .../{swap => trade}/hooks/useIsSwapEth.ts | 4 +- .../hooks/useNavigateToNewOrderCallback.ts | 8 +- .../trade/hooks/useNotifyWidgetTrade.ts | 1 + .../trade/hooks/useOrderSubmittedContent.tsx | 35 +++ .../{swap => trade}/hooks/useShouldPayGas.ts | 2 +- .../src/modules/trade/hooks/useTradeState.ts | 35 ++- .../trade/hooks/useTradeTypeInfoFromUrl.tsx | 4 +- .../trade/hooks/useUnknownImpactWarning.ts | 29 +++ .../src/modules/trade/index.ts | 14 ++ .../trade/pure/ConfirmDetailsItem/index.tsx | 4 +- .../trade/pure/ConfirmDetailsItem/styled.ts | 6 +- .../trade/pure/NoImpactWarning/index.tsx | 40 ---- .../pure/TradeConfirmation/index.cosmos.tsx | 2 +- .../trade/pure/TradeConfirmation/index.tsx | 11 +- .../ZeroApprovalWarning.cosmos.tsx | 0 .../ZeroApprovalWarning.tsx | 2 +- .../trade}/pure/ZeroApprovalWarning/index.tsx | 0 .../trade/state/derivedTradeStateAtom.ts | 5 + .../{swap => trade}/state/isEoaEthFlowAtom.ts | 0 .../src/modules/trade/types/TradeType.ts | 1 + .../trade/utils/parameterizeTradeRoute.ts | 9 +- .../modules/tradeFlow/hooks/useHandleSwap.ts | 56 +++++ .../hooks/useSafeBundleFlowContext.ts | 47 ++++ .../tradeFlow/hooks/useTradeFlowContext.ts | 192 +++++++++++++++++ .../tradeFlow/hooks/useTradeFlowType.ts | 31 +++ .../src/modules/tradeFlow/index.ts | 4 + .../services/safeBundleFlow/index.ts | 0 .../safeBundleFlow/safeBundleApprovalFlow.ts | 32 ++- .../safeBundleFlow/safeBundleEthFlow.ts | 33 +-- .../services/swapFlow/README.md | 0 .../services/swapFlow/index.ts | 18 +- .../swapFlow/steps/presignOrderStep.ts | 0 .../services/swapFlow/swapFlow.puml | 0 .../tradeFlow/types/TradeFlowContext.ts | 52 +++++ .../services/validateTradeForm.ts | 2 + .../hooks/useSetTradeQuoteParams.ts | 6 +- .../tradeQuote/hooks/useTradeQuotePolling.ts | 47 +++- .../tradeQuote/state/tradeQuoteAtom.ts | 13 +- .../tradeQuote/state/tradeQuoteParamsAtom.ts | 1 + .../HighSuggestedSlippageWarning/index.tsx | 39 ++++ .../hooks/useIsSlippageModified.ts | 0 .../hooks/useIsSmartSlippageApplied.ts | 0 .../tradeSlippage/hooks/useSetSlippage.ts | 7 + .../tradeSlippage/hooks/useTradeSlippage.ts | 22 ++ .../src/modules/tradeSlippage/index.tsx | 6 + .../state/slippageValueAndTypeAtom.ts | 32 +-- .../calculateBpsFromFeeMultiplier.test.ts | 0 .../calculateBpsFromFeeMultiplier.ts | 0 .../updaters/SmartSlippageUpdater/index.ts | 4 +- .../useSmartSlippageFromBff.ts | 0 .../useSmartSlippageFromFeeMultiplier.ts | 0 .../containers/BundleTxWrapBanner/index.tsx | 29 +++ .../containers/HighFeeWarning/consts.ts | 3 + .../HighFeeWarning/hooks/useHighFeeWarning.ts | 86 ++++++++ .../containers/HighFeeWarning/index.tsx | 89 ++++++++ .../containers/HighFeeWarning/styled.tsx | 139 ++++++++++++ .../containers/RowDeadline/index.tsx | 30 +++ .../containers/RowSlippage/index.tsx | 66 ++++++ .../containers/SettingsTab/index.tsx | 116 ++++++++++ .../containers/SettingsTab/styled.tsx | 86 ++++++++ .../containers/TradeRateDetails/index.tsx | 40 ++-- .../containers}/TransactionSettings/index.tsx | 202 ++++------------- .../containers/TransactionSettings/styled.tsx | 131 +++++++++++ .../src/modules/tradeWidgetAddons/index.ts | 7 + .../pure/NetworkCostsTooltipSuffix.tsx | 4 +- .../pure/Row/RowDeadline/index.cosmos.tsx | 1 - .../pure/Row/RowDeadline/index.tsx | 51 +++++ .../Row/RowSlippageContent/index.cosmos.tsx | 5 +- .../pure/Row/RowSlippageContent/index.tsx | 93 ++------ .../pure/Row/styled.ts | 29 ++- .../state/settingsTabState.ts | 3 + .../containers/TwapConfirmModal/index.tsx | 76 +++---- .../containers/TwapFormWarnings/index.tsx | 35 +-- .../twap/containers/TwapFormWidget/index.tsx | 11 +- .../twap/hooks/useAreWarningsAccepted.ts | 11 +- .../twap/hooks/useTwapWarningsContext.ts | 7 +- .../twap/state/twapOrdersSettingsAtom.ts | 4 +- .../modules/volumeFee/state/volumeFeeAtom.ts | 1 + .../yield/containers/TradeButtons/index.tsx | 33 +++ .../yield/containers/Warnings/index.tsx | 9 + .../containers/YieldConfirmModal/index.tsx | 100 +++++++++ .../yield/containers/YieldWidget/index.tsx | 136 ++++++++++++ .../yield/hooks/useUpdateCurrencyAmount.ts | 22 ++ .../yield/hooks/useUpdateYieldRawState.ts | 7 + .../yield/hooks/useYieldDerivedState.ts | 26 +++ .../modules/yield/hooks/useYieldRawState.ts | 7 + .../modules/yield/hooks/useYieldSettings.ts | 30 +++ .../yield/hooks/useYieldWidgetActions.ts | 47 ++++ .../src/modules/yield/index.ts | 5 + .../modules/yield/state/yieldRawStateAtom.ts | 37 ++++ .../modules/yield/state/yieldSettingsAtom.ts | 19 ++ .../updaters/QuoteObserverUpdater/index.tsx | 40 ++++ .../src/modules/yield/updaters/index.tsx | 23 ++ .../src/pages/AdvancedOrders/index.tsx | 8 +- .../src/pages/Yield/index.tsx | 10 + .../src/utils/orderUtils/getUiOrderType.ts | 1 + .../src/app/configurator/consts.ts | 2 +- .../src/app/embedDialog/const.ts | 2 +- libs/types/src/common.ts | 3 + .../src/containers/InlineBanner/banners.tsx | 52 ----- libs/ui/src/containers/InlineBanner/index.tsx | 4 +- libs/ui/src/pure/InfoTooltip/index.tsx | 5 +- libs/widget-lib/src/types.ts | 1 + 194 files changed, 2911 insertions(+), 2610 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/utils/tradeSettingsTooltips.tsx delete mode 100644 apps/cowswap-frontend/src/legacy/components/Settings/index.tsx delete mode 100644 apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/containers/Row/RowDeadline/index.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/helpers/getEthFlowEnabled.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.test.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useSetSlippage.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useSwapSlippage.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/pure/Row/types.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/pure/Row/typings.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/pure/styled.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/types/flowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx create mode 100644 apps/cowswap-frontend/src/modules/trade/containers/NoImpactWarning/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/trade/containers/TradeWarnings/index.tsx rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useIsEoaEthFlow.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useIsSafeEthFlow.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useIsSwapEth.ts (53%) rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useNavigateToNewOrderCallback.ts (83%) create mode 100644 apps/cowswap-frontend/src/modules/trade/hooks/useOrderSubmittedContent.tsx rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useShouldPayGas.ts (82%) create mode 100644 apps/cowswap-frontend/src/modules/trade/hooks/useUnknownImpactWarning.ts delete mode 100644 apps/cowswap-frontend/src/modules/trade/pure/NoImpactWarning/index.tsx rename apps/cowswap-frontend/src/{common => modules/trade}/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx (100%) rename apps/cowswap-frontend/src/{common => modules/trade}/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx (94%) rename apps/cowswap-frontend/src/{common => modules/trade}/pure/ZeroApprovalWarning/index.tsx (100%) rename apps/cowswap-frontend/src/modules/{swap => trade}/state/isEoaEthFlowAtom.ts (100%) create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowType.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/index.ts rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/safeBundleFlow/index.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/safeBundleFlow/safeBundleApprovalFlow.ts (88%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/safeBundleFlow/safeBundleEthFlow.ts (89%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/swapFlow/README.md (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/swapFlow/index.ts (92%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/swapFlow/steps/presignOrderStep.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/swapFlow/swapFlow.puml (100%) create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/hooks/useIsSlippageModified.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/hooks/useIsSmartSlippageApplied.ts (100%) create mode 100644 apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useSetSlippage.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useTradeSlippage.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeSlippage/index.tsx rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/state/slippageValueAndTypeAtom.ts (67%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/index.ts (88%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts (100%) create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/consts.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowDeadline/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/styled.tsx rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/containers/TradeRateDetails/index.tsx (72%) rename apps/cowswap-frontend/src/{legacy/components => modules/tradeWidgetAddons/containers}/TransactionSettings/index.tsx (71%) create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/index.ts rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/NetworkCostsTooltipSuffix.tsx (93%) rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/Row/RowDeadline/index.cosmos.tsx (92%) create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.tsx rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/Row/RowSlippageContent/index.cosmos.tsx (74%) rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/Row/RowSlippageContent/index.tsx (55%) rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/Row/styled.ts (73%) create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/Warnings/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/YieldConfirmModal/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useUpdateCurrencyAmount.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useUpdateYieldRawState.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useYieldDerivedState.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useYieldRawState.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/index.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/state/yieldRawStateAtom.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/updaters/QuoteObserverUpdater/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/updaters/index.tsx create mode 100644 apps/cowswap-frontend/src/pages/Yield/index.tsx diff --git a/apps/cowswap-frontend/src/common/constants/routes.ts b/apps/cowswap-frontend/src/common/constants/routes.ts index bdc140310a..37045e433f 100644 --- a/apps/cowswap-frontend/src/common/constants/routes.ts +++ b/apps/cowswap-frontend/src/common/constants/routes.ts @@ -7,6 +7,7 @@ export const Routes = { SWAP: `/:chainId?${TRADE_WIDGET_PREFIX}/swap/:inputCurrencyId?/:outputCurrencyId?`, HOOKS: `/:chainId?${TRADE_WIDGET_PREFIX}/swap/hooks/:inputCurrencyId?/:outputCurrencyId?`, LIMIT_ORDER: `/:chainId?${TRADE_WIDGET_PREFIX}/limit/:inputCurrencyId?/:outputCurrencyId?`, + YIELD: `/:chainId?${TRADE_WIDGET_PREFIX}/yield/:inputCurrencyId?/:outputCurrencyId?`, ADVANCED_ORDERS: `/:chainId?${TRADE_WIDGET_PREFIX}/advanced/:inputCurrencyId?/:outputCurrencyId?`, LONG_LIMIT_ORDER: `/:chainId?${TRADE_WIDGET_PREFIX}/limit-orders/:inputCurrencyId?/:outputCurrencyId?`, LONG_ADVANCED_ORDERS: `/:chainId?${TRADE_WIDGET_PREFIX}/advanced-orders/:inputCurrencyId?/:outputCurrencyId?`, @@ -55,3 +56,10 @@ export const HOOKS_STORE_MENU_ITEM = { description: 'Powerful tool to generate pre/post interaction for CoW Protocol', badge: 'New', } + +export const YIELD_MENU_ITEM = { + route: Routes.YIELD, + label: 'Yield', + fullLabel: 'Yield', + description: 'Provide liquidity', +} diff --git a/apps/cowswap-frontend/src/common/hooks/useGetMarketDimension.ts b/apps/cowswap-frontend/src/common/hooks/useGetMarketDimension.ts index c964124f4f..35291e9a04 100644 --- a/apps/cowswap-frontend/src/common/hooks/useGetMarketDimension.ts +++ b/apps/cowswap-frontend/src/common/hooks/useGetMarketDimension.ts @@ -9,6 +9,7 @@ const widgetTypeMap: Record = { [TradeType.LIMIT_ORDER]: 'LIMIT', // TODO: set different type for other advanced orders [TradeType.ADVANCED_ORDERS]: 'TWAP', + [TradeType.YIELD]: 'YIELD', } /**\ diff --git a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts index 2826cee38d..bf6a72af20 100644 --- a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts +++ b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts @@ -1,14 +1,25 @@ import { useMemo } from 'react' import { useFeatureFlags } from '@cowprotocol/common-hooks' -import { isLocal } from '@cowprotocol/common-utils' +import { isLocal, isPr } from '@cowprotocol/common-utils' -import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS } from '../constants/routes' +import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS, YIELD_MENU_ITEM } from '../constants/routes' export function useMenuItems() { const { isHooksStoreEnabled } = useFeatureFlags() + const { isYieldEnabled } = useFeatureFlags() return useMemo(() => { - return isHooksStoreEnabled || isLocal ? MENU_ITEMS.concat(HOOKS_STORE_MENU_ITEM) : MENU_ITEMS + const items = [...MENU_ITEMS] + + if (isHooksStoreEnabled || isLocal) { + items.push(HOOKS_STORE_MENU_ITEM) + } + + if (isYieldEnabled || isLocal || isPr) { + items.push(YIELD_MENU_ITEM) + } + + return items }, [isHooksStoreEnabled]) } diff --git a/apps/cowswap-frontend/src/common/pure/TransactionSubmittedContent/index.tsx b/apps/cowswap-frontend/src/common/pure/TransactionSubmittedContent/index.tsx index f8ca1c4d75..d1ab7fe366 100644 --- a/apps/cowswap-frontend/src/common/pure/TransactionSubmittedContent/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/TransactionSubmittedContent/index.tsx @@ -1,4 +1,5 @@ -import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { SupportedChainId, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { Command } from '@cowprotocol/types' import { BackButton } from '@cowprotocol/ui' import { Currency } from '@uniswap/sdk-core' @@ -6,12 +7,12 @@ import { Nullish } from 'types' import { DisplayLink } from 'legacy/components/TransactionConfirmationModal/DisplayLink' import { ActivityStatus } from 'legacy/hooks/useRecentActivity' +import type { Order } from 'legacy/state/orders/actions' import { ActivityDerivedState } from 'modules/account/containers/Transaction' import { GnosisSafeTxDetails } from 'modules/account/containers/Transaction/ActivityDetails' import { Category, cowAnalytics } from 'modules/analytics' import { EthFlowStepper } from 'modules/swap/containers/EthFlowStepper' -import { NavigateToNewOrderCallback } from 'modules/swap/hooks/useNavigateToNewOrderCallback' import { WatchAssetInWallet } from 'modules/wallet/containers/WatchAssetInWallet' import * as styledEl from './styled' @@ -46,7 +47,7 @@ export interface TransactionSubmittedContentProps { activityDerivedState: ActivityDerivedState | null currencyToAdd?: Nullish orderProgressBarV2Props: OrderProgressBarV2Props - navigateToNewOrderCallback?: NavigateToNewOrderCallback + navigateToNewOrderCallback?: (chainId: SupportedChainId, order?: Order, callback?: Command) => () => void } export function TransactionSubmittedContent({ diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts index 4eeb7b8030..001225192d 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts @@ -17,8 +17,8 @@ import { Field } from 'legacy/state/types' import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { useAppData } from 'modules/appData' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useDerivedSwapInfo, useSwapState } from 'modules/swap/hooks/useSwapState' +import { useIsEoaEthFlow } from 'modules/trade' import { isRefetchQuoteRequired } from './isRefetchQuoteRequired' import { quoteUsingSameParameters } from './quoteUsingSameParameters' diff --git a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts index bb35f5283b..91e589f3a7 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts @@ -57,7 +57,7 @@ async function _updatePresignGnosisSafeTx( getSafeTxInfo: GetSafeTxInfo, updatePresignGnosisSafeTx: UpdatePresignGnosisSafeTxCallback, cancelOrdersBatch: CancelOrdersBatchCallback, - safeInfo: GnosisSafeInfo | undefined + safeInfo: GnosisSafeInfo | undefined, ) { const getSafeTxPromises = allPendingOrders // Update orders that are pending for presingature @@ -100,7 +100,7 @@ async function _updatePresignGnosisSafeTx( if (!error.isCancelledError) { console.error( `[PendingOrdersUpdater] Failed to check Gnosis Safe tx hash: ${presignGnosisSafeTxHash}`, - error + error, ) } }) @@ -113,7 +113,7 @@ async function _updateCreatingOrders( chainId: ChainId, pendingOrders: Order[], isSafeWallet: boolean, - addOrUpdateOrders: AddOrUpdateOrdersCallback + addOrUpdateOrders: AddOrUpdateOrdersCallback, ): Promise { const promises = pendingOrders.reduce[]>((acc, order) => { if (order.status === OrderStatus.CREATING) { @@ -205,7 +205,7 @@ async function _updateOrders({ // Iterate over pending orders fetching API data const unfilteredOrdersData = await Promise.all( - pending.map(async (orderFromStore) => fetchAndClassifyOrder(orderFromStore, chainId)) + pending.map(async (orderFromStore) => fetchAndClassifyOrder(orderFromStore, chainId)), ) // Group resolved promises by status @@ -219,7 +219,7 @@ async function _updateOrders({ } return acc }, - { fulfilled: [], expired: [], cancelled: [], unknown: [], presigned: [], pending: [], presignaturePending: [] } + { fulfilled: [], expired: [], cancelled: [], unknown: [], presigned: [], pending: [], presignaturePending: [] }, ) if (presigned.length > 0) { @@ -310,7 +310,7 @@ async function _updateOrders({ getSafeTxInfo, updatePresignGnosisSafeTx, cancelOrdersBatch, - safeInfo + safeInfo, ) // Update the creating EthFlow orders (if any) await _updateCreatingOrders(chainId, orders, isSafeWallet, addOrUpdateOrders) @@ -318,7 +318,7 @@ async function _updateOrders({ function getReplacedOrCancelledEthFlowOrders( orders: Order[], - allTransactions: UpdateOrdersParams['allTransactions'] + allTransactions: UpdateOrdersParams['allTransactions'], ): Order[] { return orders.filter((order) => { if (!order.orderCreationHash || order.status !== OrderStatus.CREATING) return false @@ -373,6 +373,7 @@ export function PendingOrdersUpdater(): null { const isUpdatingLimit = useRef(false) const isUpdatingTwap = useRef(false) const isUpdatingHooks = useRef(false) + const isUpdatingYield = useRef(false) const updatersRefMap = useMemo( () => ({ @@ -380,8 +381,9 @@ export function PendingOrdersUpdater(): null { [UiOrderType.LIMIT]: isUpdatingLimit, [UiOrderType.TWAP]: isUpdatingTwap, [UiOrderType.HOOKS]: isUpdatingHooks, + [UiOrderType.YIELD]: isUpdatingYield, }), - [] + [], ) // Ref, so we don't rerun useEffect @@ -411,7 +413,7 @@ export function PendingOrdersUpdater(): null { // Remove orders from the cancelling queue (marked by checkbox in the orders table) removeOrdersToCancel(fulfillOrdersBatchParams.orders.map(({ uid }) => uid)) }, - [chainId, _fulfillOrdersBatch, removeOrdersToCancel] + [chainId, _fulfillOrdersBatch, removeOrdersToCancel], ) const updateOrders = useCallback( @@ -460,7 +462,7 @@ export function PendingOrdersUpdater(): null { getSafeTxInfo, safeInfo, allTransactions, - ] + ], ) useEffect(() => { @@ -470,15 +472,15 @@ export function PendingOrdersUpdater(): null { const marketInterval = setInterval( () => updateOrders(chainId, account, isSafeWallet, UiOrderType.SWAP), - MARKET_OPERATOR_API_POLL_INTERVAL + MARKET_OPERATOR_API_POLL_INTERVAL, ) const limitInterval = setInterval( () => updateOrders(chainId, account, isSafeWallet, UiOrderType.LIMIT), - LIMIT_OPERATOR_API_POLL_INTERVAL + LIMIT_OPERATOR_API_POLL_INTERVAL, ) const twapInterval = setInterval( () => updateOrders(chainId, account, isSafeWallet, UiOrderType.TWAP), - LIMIT_OPERATOR_API_POLL_INTERVAL + LIMIT_OPERATOR_API_POLL_INTERVAL, ) updateOrders(chainId, account, isSafeWallet, UiOrderType.SWAP) diff --git a/apps/cowswap-frontend/src/common/utils/tradeSettingsTooltips.tsx b/apps/cowswap-frontend/src/common/utils/tradeSettingsTooltips.tsx new file mode 100644 index 0000000000..f48fb7aacb --- /dev/null +++ b/apps/cowswap-frontend/src/common/utils/tradeSettingsTooltips.tsx @@ -0,0 +1,61 @@ +import { + INPUT_OUTPUT_EXPLANATION, + MINIMUM_ETH_FLOW_DEADLINE_SECONDS, + MINIMUM_ETH_FLOW_SLIPPAGE, + PERCENTAGE_PRECISION, +} from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' + +import { Trans } from '@lingui/macro' + +export function getNativeOrderDeadlineTooltip(symbols: (string | undefined)[] | undefined) { + return ( + + {symbols?.[0] || 'Native currency (e.g ETH)'} orders require a minimum transaction expiration time threshold of{' '} + {MINIMUM_ETH_FLOW_DEADLINE_SECONDS / 60} minutes to ensure the best swapping experience. +
+
+ Orders not matched after the threshold time are automatically refunded. +
+ ) +} + +export function getNonNativeOrderDeadlineTooltip() { + return ( + + Your swap expires and will not execute if it is pending for longer than the selected duration. +
+
+ {INPUT_OUTPUT_EXPLANATION} +
+ ) +} + +export const getNativeSlippageTooltip = (chainId: SupportedChainId, symbols: (string | undefined)[] | undefined) => ( + + When selling {symbols?.[0] || 'a native currency'}, the minimum slippage tolerance is set to{' '} + {MINIMUM_ETH_FLOW_SLIPPAGE[chainId].toSignificant(PERCENTAGE_PRECISION)}% to ensure a high likelihood of order + matching, even in volatile market conditions. +
+
+ {symbols?.[0] || 'Native currency'} orders can, in rare cases, be frontrun due to their on-chain component. For more + robust MEV protection, consider wrapping your {symbols?.[0] || 'native currency'} before trading. +
+) + +export const getNonNativeSlippageTooltip = (isSettingsModal?: boolean) => ( + + CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting the + best price.{' '} + {isSettingsModal ? ( + <> + To override this, enter your desired slippage amount. +
+
+ Either way, your slippage is protected from MEV! + + ) : ( + "Trades are protected from MEV, so your slippage can't be exploited!" + )} +
+) diff --git a/apps/cowswap-frontend/src/legacy/components/Settings/index.tsx b/apps/cowswap-frontend/src/legacy/components/Settings/index.tsx deleted file mode 100644 index 9ac795d417..0000000000 --- a/apps/cowswap-frontend/src/legacy/components/Settings/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useCallback, useRef } from 'react' - -import { useOnClickOutside } from '@cowprotocol/common-hooks' -import { HelpTooltip, Media, RowBetween, RowFixed, UI } from '@cowprotocol/ui' - -import { Trans } from '@lingui/macro' -import { transparentize } from 'color2k' -import { Text } from 'rebass' -import styled from 'styled-components/macro' -import { ThemedText } from 'theme' - -import { AutoColumn } from 'legacy/components/Column' -import { Toggle } from 'legacy/components/Toggle' -import { TransactionSettings } from 'legacy/components/TransactionSettings' -import { useModalIsOpen, useToggleSettingsMenu } from 'legacy/state/application/hooks' -import { ApplicationModal } from 'legacy/state/application/reducer' -import { useRecipientToggleManager } from 'legacy/state/user/hooks' - -import { toggleRecipientAddressAnalytics } from 'modules/analytics' -import { SettingsIcon } from 'modules/trade/pure/Settings' - -export const StyledMenuButton = styled.button` - position: relative; - width: 100%; - border: none; - background-color: transparent; - margin: 0; - padding: 0; - border-radius: 0.5rem; - height: var(${UI.ICON_SIZE_NORMAL}); - opacity: 0.6; - transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; - color: inherit; - display: flex; - align-items: center; - - &:hover, - &:focus { - opacity: 1; - cursor: pointer; - outline: none; - color: currentColor; - } - - svg { - opacity: 1; - margin: auto; - transition: transform 0.3s cubic-bezier(0.65, 0.05, 0.36, 1); - color: inherit; - } -` - -const StyledMenu = styled.div` - margin: 0; - display: flex; - justify-content: center; - align-items: center; - position: relative; - border: none; - text-align: left; - color: inherit; - - ${RowFixed} { - color: inherit; - - > div { - color: inherit; - opacity: 0.85; - } - } -` - -export const MenuFlyout = styled.span` - min-width: 20.125rem; - background: var(${UI.COLOR_PRIMARY}); - box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), - 0px 24px 32px rgba(0, 0, 0, 0.01); - border-radius: 12px; - display: flex; - flex-direction: column; - font-size: 1rem; - position: absolute; - z-index: 100; - color: inherit; - box-shadow: ${({ theme }) => theme.boxShadow2}; - border: 1px solid ${({ theme }) => transparentize(theme.white, 0.95)}; - background-color: var(${UI.COLOR_PAPER}); - color: inherit; - padding: 0; - margin: 0; - top: 36px; - right: 0; - width: 280px; - - ${Media.upToMedium()} { - min-width: 18.125rem; - } - - user-select: none; -` - -interface SettingsTabProps { - className?: string -} - -export function SettingsTab({ className }: SettingsTabProps) { - const node = useRef(null) - const open = useModalIsOpen(ApplicationModal.SETTINGS) - const toggle = useToggleSettingsMenu() - - const [recipientToggleVisible, toggleRecipientVisibilityAux] = useRecipientToggleManager() - const toggleRecipientVisibility = useCallback( - (value?: boolean) => { - const isVisible = value ?? !recipientToggleVisible - toggleRecipientAddressAnalytics(isVisible) - toggleRecipientVisibilityAux(isVisible) - }, - [toggleRecipientVisibilityAux, recipientToggleVisible] - ) - - // show confirmation view before turning on - - useOnClickOutside([node], open ? toggle : undefined) - - return ( - // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 - - - - - {open && ( - - - - Transaction Settings - - - - Interface Settings - - - - - - Custom Recipient - - Allows you to choose a destination address for the swap other than the connected one. - } - /> - - - - - - )} - - ) -} diff --git a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx deleted file mode 100644 index 8fdc4411e9..0000000000 --- a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { Command } from '@cowprotocol/types' -import { HoverTooltip } from '@cowprotocol/ui' -import { Fraction } from '@uniswap/sdk-core' - -import { AlertTriangle } from 'react-feather' -import styled from 'styled-components/macro' - -import TradeGp from 'legacy/state/swap/TradeGp' -import { useIsDarkMode } from 'legacy/state/user/hooks' - -import { useHighFeeWarning } from 'modules/swap/hooks/useSwapState' -import { StyledInfoIcon } from 'modules/swap/pure/styled' - -import { useSafeMemo } from 'common/hooks/useSafeMemo' - -import { AuxInformationContainer } from '../swap/styleds' - -interface HighFeeContainerProps { - padding?: string - margin?: string - width?: string - level?: number - isDarkMode?: boolean -} - -const WarningCheckboxContainer = styled.span` - display: flex; - width: 100%; - margin: 0 auto; - font-weight: bold; - gap: 2px; - justify-content: center; - align-items: center; - border-radius: 16px; - padding: 0; - margin: 10px auto; - - > input { - cursor: pointer; - margin: 1px 4px 0 0; - } -` - -const WarningContainer = styled(AuxInformationContainer).attrs((props) => ({ - ...props, - hideInput: true, -}))` - --warningColor: ${({ theme, level }) => - level === HIGH_TIER_FEE - ? theme.danger - : level === MEDIUM_TIER_FEE - ? theme.warning - : LOW_TIER_FEE - ? theme.alert - : theme.info}; - color: inherit; - padding: ${({ padding = '16px' }) => padding}; - width: ${({ width = '100%' }) => width}; - border-radius: 16px; - border: 0; - margin: ${({ margin = '0 auto' }) => margin}; - position: relative; - z-index: 1; - - &:hover { - border: 0; - } - - &::before { - content: ''; - display: block; - position: absolute; - top: 0; - left: 0; - border-radius: inherit; - background: var(--warningColor); - opacity: ${({ isDarkMode }) => (isDarkMode ? 0.2 : 0.15)}; - z-index: -1; - width: 100%; - height: 100%; - pointer-events: none; - } - - > div { - display: flex; - justify-content: center; - align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 500; - text-align: center; - - > svg:first-child { - stroke: var(--warningColor); - } - } -` - -const ErrorStyledInfoIcon = styled(StyledInfoIcon)` - color: ${({ theme }) => (theme.darkMode ? '#FFCA4A' : '#564D00')}; -` -const HIGH_TIER_FEE = 30 -const MEDIUM_TIER_FEE = 20 -const LOW_TIER_FEE = 10 - -// checks fee as percentage (30% not a decimal) -function _getWarningInfo(feePercentage?: Fraction) { - if (!feePercentage || feePercentage.lessThan(LOW_TIER_FEE)) { - return undefined - } else if (feePercentage.lessThan(MEDIUM_TIER_FEE)) { - return LOW_TIER_FEE - } else if (feePercentage.lessThan(HIGH_TIER_FEE)) { - return MEDIUM_TIER_FEE - } else { - return HIGH_TIER_FEE - } -} - -const HighFeeWarningMessage = ({ feePercentage }: { feePercentage?: Fraction }) => ( -
- - Current network costs make up{' '} - - {feePercentage?.toFixed(2)}% - {' '} - of your swap amount. -
-
- Consider waiting for lower network costs. -
-
- You may still move forward with this swap but a high percentage of it will be consumed by network costs. -
-
-) - -export type WarningProps = { - trade?: TradeGp - acceptedStatus?: boolean - className?: string - acceptWarningCb?: Command - hide?: boolean -} & HighFeeContainerProps - -export const HighFeeWarning = (props: WarningProps) => { - const { acceptedStatus, acceptWarningCb, trade } = props - const darkMode = useIsDarkMode() - - const { isHighFee, feePercentage } = useHighFeeWarning(trade) - const level = useSafeMemo(() => _getWarningInfo(feePercentage), [feePercentage]) - - if (!isHighFee) return null - - return ( - -
- - Costs exceed {level}% of the swap amount!{' '} - }> - - {' '} -
- - {acceptWarningCb && ( - - Swap - anyway - - )} -
- ) -} - -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 ( - -
- - Slippage adjusted to {`${slippageBps / 100}`}% to ensure quick execution - - - -
-
- ) -} diff --git a/apps/cowswap-frontend/src/legacy/components/swap/styleds.tsx b/apps/cowswap-frontend/src/legacy/components/swap/styleds.tsx index 9dbb80ca71..7a1ae3d4be 100644 --- a/apps/cowswap-frontend/src/legacy/components/swap/styleds.tsx +++ b/apps/cowswap-frontend/src/legacy/components/swap/styleds.tsx @@ -1,109 +1,11 @@ -import { Media, UI } from '@cowprotocol/ui' - import styled from 'styled-components/macro' -const FeeInformationTooltipWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - height: 60px; -` - export const Container = styled.div` max-width: 460px; width: 100%; ` + export const Wrapper = styled.div` position: relative; padding: 8px; ` - -// TODO: refactor these styles -export const AuxInformationContainer = styled.div<{ - margin?: string - borderColor?: string - borderWidth?: string - hideInput: boolean - disabled?: boolean - showAux?: boolean -}>` - border: 1px solid ${({ hideInput }) => (hideInput ? ' transparent' : `var(${UI.COLOR_PAPER_DARKER})`)}; - background-color: var(${UI.COLOR_PAPER}); - width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; - - :focus, - :hover { - border: 1px solid ${({ theme, hideInput }) => (hideInput ? ' transparent' : theme.background)}; - } - - ${({ theme, hideInput, disabled }) => - !disabled && - ` - :focus, - :hover { - border: 1px solid ${hideInput ? ' transparent' : theme.background}; - } - `} - - margin: ${({ margin = '0 auto' }) => margin}; - border-radius: 0 0 15px 15px; - border: 2px solid var(${UI.COLOR_PAPER_DARKER}); - - &:hover { - border: 2px solid var(${UI.COLOR_PAPER_DARKER}); - } - - ${Media.upToSmall()} { - height: auto; - flex-flow: column wrap; - justify-content: flex-end; - align-items: flex-end; - } - > ${FeeInformationTooltipWrapper} { - align-items: center; - justify-content: space-between; - margin: 0 16px; - padding: 16px 0; - font-weight: 600; - font-size: 14px; - height: auto; - - ${Media.upToSmall()} { - flex-flow: column wrap; - width: 100%; - align-items: flex-start; - margin: 0; - padding: 16px; - } - - > span { - font-size: 18px; - gap: 2px; - word-break: break-all; - text-align: right; - - ${Media.upToSmall()} { - text-align: left; - align-items: flex-start; - width: 100%; - } - } - - > span:first-child { - font-size: 14px; - display: flex; - align-items: center; - white-space: nowrap; - - ${Media.upToSmall()} { - margin: 0 0 10px; - } - } - - > span > small { - opacity: 0.75; - font-size: 13px; - font-weight: 500; - } - } -` diff --git a/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/types.ts b/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/types.ts index 8c8ae12483..5680fe9f06 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/types.ts +++ b/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/types.ts @@ -4,17 +4,3 @@ export type ParsedAmounts = { INPUT: CurrencyAmount | undefined OUTPUT: CurrencyAmount | undefined } - -export interface PriceImpactTrade { - inputAmount: CurrencyAmount | null - outputAmount: CurrencyAmount | null - inputAmountWithoutFee?: CurrencyAmount - outputAmountWithoutFee?: CurrencyAmount -} - -export interface FallbackPriceImpactParams { - abTrade?: PriceImpactTrade - isWrapping: boolean - sellToken: string | null - buyToken: string | null -} diff --git a/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx b/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx index af1d89d539..9c0144dfd7 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx +++ b/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx @@ -19,7 +19,7 @@ import { LegacyFeeQuoteParams, LegacyQuoteParams } from 'legacy/state/price/type import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { getBestQuote, getFastQuote, QuoteResult } from 'legacy/utils/price' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' +import { useIsEoaEthFlow } from 'modules/trade' import { ApiErrorCodes, isValidOperatorError } from 'api/cowProtocol/errors/OperatorError' import QuoteApiError, { @@ -183,8 +183,8 @@ export function useRefetchQuoteCallback() { const previouslyUnsupportedToken = getIsUnsupportedToken(sellToken) ? sellToken : getIsUnsupportedToken(buyToken) - ? buyToken - : null + ? buyToken + : null // can be a previously unsupported token which is now valid // so we check against map and remove it if (previouslyUnsupportedToken) { @@ -263,6 +263,6 @@ export function useRefetchQuoteCallback() { removeGpUnsupportedToken, addUnsupportedToken, setQuoteError, - ] + ], ) } diff --git a/apps/cowswap-frontend/src/legacy/state/application/hooks.ts b/apps/cowswap-frontend/src/legacy/state/application/hooks.ts index 67b8e196a9..ca72c2b086 100644 --- a/apps/cowswap-frontend/src/legacy/state/application/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/application/hooks.ts @@ -31,10 +31,6 @@ export function useToggleWalletModal(): Command { return useToggleModal(ApplicationModal.WALLET) } -export function useToggleSettingsMenu(): Command { - return useToggleModal(ApplicationModal.SETTINGS) -} - // TODO: These two seem to be gone from original. Check whether they have been replaced export function useOpenModal(modal: ApplicationModal): Command { const dispatch = useAppDispatch() diff --git a/apps/cowswap-frontend/src/legacy/state/application/reducer.ts b/apps/cowswap-frontend/src/legacy/state/application/reducer.ts index 6eabdf1cad..18840cb75f 100644 --- a/apps/cowswap-frontend/src/legacy/state/application/reducer.ts +++ b/apps/cowswap-frontend/src/legacy/state/application/reducer.ts @@ -4,7 +4,6 @@ import { initialState } from './initialState' export enum ApplicationModal { NETWORK_SELECTOR, - SETTINGS, WALLET, // ----------------- MOD: CowSwap specific modals -------------------- TRANSACTION_ERROR, @@ -16,7 +15,7 @@ export enum ApplicationModal { } export interface ApplicationState { - readonly openModal: ApplicationModal | null + openModal: ApplicationModal | null } const applicationSlice = createSlice({ @@ -29,5 +28,4 @@ const applicationSlice = createSlice({ }, }) -export const { setOpenModal } = applicationSlice.actions export default applicationSlice.reducer diff --git a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx index f347ddb113..c24357a2fb 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx +++ b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx @@ -55,13 +55,11 @@ export function useUserLocaleManager(): [SupportedLocale | null, (newLocale: Sup return [locale, setLocale] } -// TODO: mod, move to mod file export function useIsRecipientToggleVisible(): boolean { return useAppSelector((state) => state.user.recipientToggleVisible) } -// TODO: mod, move to mod file -export function useRecipientToggleManager(): [boolean, (value?: boolean) => void] { +export function useRecipientToggleManager(): [boolean, (value: boolean) => void] { const dispatch = useAppDispatch() const isVisible = useIsRecipientToggleVisible() const onChangeRecipient = useCallback( @@ -72,14 +70,14 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void ) const toggleVisibility = useCallback( - (value?: boolean) => { - const newIsVisible = value ?? !isVisible + (value: boolean) => { + const newIsVisible = value dispatch(updateRecipientToggleVisible({ recipientToggleVisible: newIsVisible })) if (!newIsVisible) { onChangeRecipient(null) } }, - [isVisible, dispatch, onChangeRecipient], + [dispatch, onChangeRecipient], ) return [isVisible, toggleVisibility] diff --git a/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx index 83b4bb4d89..19f1c6c246 100644 --- a/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx @@ -1,5 +1,5 @@ import { useAtomValue } from 'jotai' -import { PropsWithChildren, ReactNode } from 'react' +import { ReactNode } from 'react' import { isSellOrder } from '@cowprotocol/common-utils' @@ -38,12 +38,13 @@ export type AdvancedOrdersWidgetParams = { disablePriceImpact: boolean } -export type AdvancedOrdersWidgetProps = PropsWithChildren<{ +export type AdvancedOrdersWidgetProps = { updaters?: ReactNode params: AdvancedOrdersWidgetParams mapCurrencyInfo?: (info: CurrencyInfo) => CurrencyInfo confirmContent: JSX.Element -}> + children(warnings: ReactNode): ReactNode +} export function AdvancedOrdersWidget({ children, @@ -98,7 +99,9 @@ export function AdvancedOrdersWidget({ const slots: TradeWidgetSlots = { settingsWidget: , - bottomContent: children, + bottomContent(warnings) { + return children(warnings) + }, updaters, lockScreen: isUnlocked ? undefined : ( = { [UiOrderType.SWAP]: 'Market Order', [UiOrderType.TWAP]: 'TWAP Order', [UiOrderType.HOOKS]: 'Hooks', + [UiOrderType.YIELD]: 'Yield', } function getClassLabel(orderClass: UiOrderType, label?: string) { diff --git a/apps/cowswap-frontend/src/modules/appData/updater/AppDataInfoUpdater.ts b/apps/cowswap-frontend/src/modules/appData/updater/AppDataInfoUpdater.ts index eb91c3cc43..83f01614cc 100644 --- a/apps/cowswap-frontend/src/modules/appData/updater/AppDataInfoUpdater.ts +++ b/apps/cowswap-frontend/src/modules/appData/updater/AppDataInfoUpdater.ts @@ -88,6 +88,7 @@ export function AppDataInfoUpdater({ typedHooks, volumeFee, replacedOrderUid, + isSmartSlippage, ]) } diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx index 327fe74254..7469615b85 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx @@ -18,13 +18,14 @@ import { RedirectPathToSwapOnly, RedirectToPath } from 'legacy/pages/Swap/redire import { Routes as RoutesEnum, RoutesValues } from 'common/constants/routes' import Account, { AccountOverview } from 'pages/Account' +import AdvancedOrdersPage from 'pages/AdvancedOrders' import AnySwapAffectedUsers from 'pages/error/AnySwapAffectedUsers' import { HooksPage } from 'pages/Hooks' +import LimitOrderPage from 'pages/LimitOrders' import { SwapPage } from 'pages/Swap' +import YieldPage from 'pages/Yield' // Async routes -const LimitOrders = lazy(() => import(/* webpackChunkName: "limit_orders" */ 'pages/LimitOrders')) -const AdvancedOrders = lazy(() => import(/* webpackChunkName: "advanced_orders" */ 'pages/AdvancedOrders')) const NotFound = lazy(() => import(/* webpackChunkName: "not_found" */ 'pages/error/NotFound')) const CowRunner = lazy(() => import(/* webpackChunkName: "cow_runner" */ 'pages/games/CowRunner')) const MevSlicer = lazy(() => import(/* webpackChunkName: "mev_slicer" */ 'pages/games/MevSlicer')) @@ -51,9 +52,10 @@ function LazyRoute({ route, element, key }: LazyRouteProps) { } const lazyRoutes: LazyRouteProps[] = [ - { route: RoutesEnum.LIMIT_ORDER, element: }, + { route: RoutesEnum.LIMIT_ORDER, element: }, + { route: RoutesEnum.YIELD, element: }, { route: RoutesEnum.LONG_LIMIT_ORDER, element: }, - { route: RoutesEnum.ADVANCED_ORDERS, element: }, + { route: RoutesEnum.ADVANCED_ORDERS, element: }, { route: RoutesEnum.LONG_ADVANCED_ORDERS, element: }, { route: RoutesEnum.ABOUT, element: }, { route: RoutesEnum.FAQ, element: }, diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx index f8eb2cf33e..f678cefe26 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx @@ -78,6 +78,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho inputCurrencyId, outputCurrencyId, isDarkMode, + orderParams, ]) const dappProps = useMemo(() => ({ context, dapp, isPreHook }), [context, dapp, isPreHook]) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts index b3e113a014..d5bafe2f30 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts @@ -96,7 +96,7 @@ export function useRescueFundsFromProxy( } finally { setTxSigningInProgress(false) } - }, [provider, proxyAddress, cowShedContract, selectedTokenAddress, account, tokenBalance]) + }, [provider, proxyAddress, cowShedContract, selectedTokenAddress, account, tokenBalance, cowShedHooks]) return { callback, isTxSigningInProgress, proxyAddress } } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx index 800da41ee1..ef3a5adb4a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx @@ -52,7 +52,7 @@ export function TenderlySimulate({ hook }: TenderlySimulateProps) { } finally { setIsLoading(false) } - }, [simulate, hook, hookId]) + }, [simulate, hook, hookId, setSimulationError]) if (isLoading) { return ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx index ffeed661bd..55a7afffe2 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx @@ -74,7 +74,7 @@ export function BuildHookApp({ context }: HookDappProps) { hook, }) : context.addHook({ hook }) - }, [hook, context, hookToEdit, isPreHook]) + }, [hook, context, hookToEdit]) return ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx index 6c9be054d0..e5d24ee68f 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx @@ -38,7 +38,7 @@ export function ClaimGnoHookApp({ context }: HookDappProps) { } return SbcDepositContractInterface.encodeFunctionData('claimWithdrawal', [account]) - }, [context]) + }, [context, account]) useEffect(() => { if (!account || !provider) { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx index a619b317c6..2f29f33527 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx @@ -42,7 +42,7 @@ export function PermitHookApp({ context }: HookDappProps) { } context.addHook({ hook }) - }, [generatePermitHook, context, permitInfo, token, spenderAddress]) + }, [generatePermitHook, context, permitInfo, token, spenderAddress, hookToEdit]) const buttonProps = useMemo(() => { if (!context.account) return { message: 'Connect wallet', disabled: true } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts index b0e012b116..a3bf438e4b 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts @@ -33,6 +33,6 @@ export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { } }) }, - [updateHooks, dapp], + [updateHooks, dapp, isPreHook], ) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts index 477f8cb68e..531411e864 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts @@ -25,6 +25,6 @@ export function useRemoveHook(isPreHook: boolean): RemoveHook { } }) }, - [updateHooks], + [updateHooks, isPreHook], ) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts index 63fb6bbe4e..2df029bc11 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts @@ -19,5 +19,5 @@ export function useSetRecipientOverride() { if (!hookRecipientOverride || !isHooksTradeType) return onChangeRecipient(hookRecipientOverride) - }, [hookRecipientOverride, isHooksTradeType, isNativeIn]) + }, [hookRecipientOverride, isHooksTradeType, isNativeIn, onChangeRecipient]) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts index 7f531ad420..d77e7e1907 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts @@ -2,24 +2,26 @@ import { useEffect } from 'react' import { getCurrencyAddress } from '@cowprotocol/common-utils' -import { useSwapFlowContext } from 'modules/swap/hooks/useSwapFlowContext' - import { useSetOrderParams } from './useSetOrderParams' +import { useSwapFlowContext } from '../../swap/hooks/useSwapFlowContext' + export function useSetupHooksStoreOrderParams() { - const swapFlowContext = useSwapFlowContext() + const tradeFlowContext = useSwapFlowContext() const setOrderParams = useSetOrderParams() - const orderParams = swapFlowContext?.orderParams + const orderParams = tradeFlowContext?.orderParams useEffect(() => { - if (!orderParams) return - - setOrderParams({ - validTo: orderParams.validTo, - sellAmount: orderParams.inputAmount.quotient.toString(), - buyAmount: orderParams.outputAmount.quotient.toString(), - sellTokenAddress: getCurrencyAddress(orderParams.inputAmount.currency), - buyTokenAddress: getCurrencyAddress(orderParams.outputAmount.currency), - }) - }, [orderParams]) + if (!orderParams) { + setOrderParams(null) + } else { + setOrderParams({ + validTo: orderParams.validTo, + sellAmount: orderParams.inputAmount.quotient.toString(), + buyAmount: orderParams.outputAmount.quotient.toString(), + sellTokenAddress: getCurrencyAddress(orderParams.inputAmount.currency), + buyTokenAddress: getCurrencyAddress(orderParams.outputAmount.currency), + }) + } + }, [orderParams, setOrderParams]) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts index e3d41b223d..64505270cb 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts @@ -20,7 +20,7 @@ export function useTenderlySimulate(): (params: CowHook) => Promise { isRequestRelevant = false } - }, [input, chainId]) + }, [input, isSmartContractWallet, chainId]) return null } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx b/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx index 6a5d950898..82e97b64c4 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx @@ -3,7 +3,6 @@ import { useAtomValue } from 'jotai/index' import { useCallback, useEffect, useMemo } from 'react' import { HookDappBase, HookDappType } from '@cowprotocol/hook-dapp-lib' -import { useWalletInfo } from '@cowprotocol/wallet' import ms from 'ms.macro' @@ -21,7 +20,6 @@ const getLastUpdateTimestamp = () => { export function IframeDappsManifestUpdater() { const hooksState = useAtomValue(customHookDappsAtom) const upsertCustomHookDapp = useSetAtom(upsertCustomHookDappAtom) - const { chainId } = useWalletInfo() const [preHooks, postHooks] = useMemo( () => [Object.values(hooksState.pre), Object.values(hooksState.post)], @@ -55,7 +53,7 @@ export function IframeDappsManifestUpdater() { } }) }, - [chainId, upsertCustomHookDapp], + [upsertCustomHookDapp], ) /** diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx index 8f3c936e00..ec34c608a1 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx @@ -2,14 +2,12 @@ import { useAtomValue, useSetAtom } from 'jotai' import React, { useCallback, useEffect } from 'react' import { isFractionFalsy } from '@cowprotocol/common-utils' -import { BundleTxApprovalBanner, BundleTxSafeWcBanner, SmallVolumeWarningBanner } from '@cowprotocol/ui' -import { useIsSafeViaWc, useWalletInfo } from '@cowprotocol/wallet' +import { SmallVolumeWarningBanner } from '@cowprotocol/ui' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import styled from 'styled-components/macro' import { Nullish } from 'types' -import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' import { useLimitOrdersFormState } from 'modules/limitOrders/hooks/useLimitOrdersFormState' import { useRateImpact } from 'modules/limitOrders/hooks/useRateImpact' @@ -17,16 +15,12 @@ import { limitOrdersWarningsAtom, updateLimitOrdersWarningsAtom, } from 'modules/limitOrders/state/limitOrdersWarningsAtom' -import { useTradePriceImpact } from 'modules/trade' import { SellNativeWarningBanner } from 'modules/trade/containers/SellNativeWarningBanner' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { useGetTradeFormValidation } from 'modules/tradeFormValidation' import { TradeFormValidation } from 'modules/tradeFormValidation/types' import { useTradeQuote } from 'modules/tradeQuote' -import { useShouldZeroApprove } from 'modules/zeroApproval' import { HIGH_FEE_WARNING_PERCENTAGE } from 'common/constants/common' -import { ZeroApprovalWarning } from 'common/pure/ZeroApprovalWarning' import { calculatePercentageInRelationToReference } from 'utils/orderUtils/calculatePercentageInRelationToReference' import { RateImpactWarning } from '../../pure/RateImpactWarning' @@ -45,9 +39,6 @@ const Wrapper = styled.div` gap: 10px; ` -const StyledNoImpactWarning = styled(NoImpactWarning)` - margin: 10px auto 0; -` const StyledRateImpactWarning = styled(RateImpactWarning)` margin: 10px auto 0; ` @@ -55,23 +46,18 @@ const StyledRateImpactWarning = styled(RateImpactWarning)` export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { const { feeAmount, isConfirmScreen = false, className } = props - const { isPriceImpactAccepted, isRateImpactAccepted } = useAtomValue(limitOrdersWarningsAtom) + const { isRateImpactAccepted } = useAtomValue(limitOrdersWarningsAtom) const updateLimitOrdersWarnings = useSetAtom(updateLimitOrdersWarningsAtom) const localFormValidation = useLimitOrdersFormState() const primaryFormValidation = useGetTradeFormValidation() const rateImpact = useRateImpact() - const { account } = useWalletInfo() - const { slippageAdjustedSellAmount, inputCurrency, inputCurrencyAmount, outputCurrency, outputCurrencyAmount } = - useLimitOrdersDerivedState() + const { inputCurrency, inputCurrencyAmount, outputCurrencyAmount } = useLimitOrdersDerivedState() const tradeQuote = useTradeQuote() - const priceImpactParams = useTradePriceImpact() - const { banners: widgetBanners } = useInjectedWidgetParams() const isBundling = primaryFormValidation && FORM_STATES_TO_SHOW_BUNDLE_BANNER.includes(primaryFormValidation) const canTrade = localFormValidation === null && (primaryFormValidation === null || isBundling) && !tradeQuote.error - const showPriceImpactWarning = canTrade && !!account && !priceImpactParams.loading && !priceImpactParams.priceImpact const showRateImpactWarning = canTrade && inputCurrency && !isFractionFalsy(inputCurrencyAmount) && !isFractionFalsy(outputCurrencyAmount) @@ -80,34 +66,10 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { const showHighFeeWarning = feePercentage?.greaterThan(HIGH_FEE_WARNING_PERCENTAGE) - const showApprovalBundlingBanner = !isConfirmScreen && isBundling - const shouldZeroApprove = useShouldZeroApprove(slippageAdjustedSellAmount) - const showZeroApprovalWarning = shouldZeroApprove && outputCurrency !== null // Show warning only when output currency is also present. - - const isSafeViaWc = useIsSafeViaWc() - const showSafeWcBundlingBanner = - !isConfirmScreen && - !showApprovalBundlingBanner && - isSafeViaWc && - primaryFormValidation === TradeFormValidation.ApproveRequired && - !widgetBanners?.hideSafeWebAppBanner - // TODO: implement Safe App EthFlow bundling for LIMIT and disable the warning in that case const showNativeSellWarning = primaryFormValidation === TradeFormValidation.SellNativeToken - const isVisible = - showPriceImpactWarning || - rateImpact < 0 || - showHighFeeWarning || - showApprovalBundlingBanner || - showSafeWcBundlingBanner || - shouldZeroApprove || - showNativeSellWarning - - // Reset price impact flag when there is no price impact - useEffect(() => { - updateLimitOrdersWarnings({ isPriceImpactAccepted: !showPriceImpactWarning }) - }, [showPriceImpactWarning, updateLimitOrdersWarnings]) + const isVisible = rateImpact < 0 || showHighFeeWarning || showNativeSellWarning // Reset rate impact before opening confirmation screen useEffect(() => { @@ -115,9 +77,6 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { updateLimitOrdersWarnings({ isRateImpactAccepted: false }) } }, [updateLimitOrdersWarnings, isConfirmScreen]) - const onAcceptPriceImpact = useCallback(() => { - updateLimitOrdersWarnings({ isPriceImpactAccepted: !isPriceImpactAccepted }) - }, [updateLimitOrdersWarnings, isPriceImpactAccepted]) const onAcceptRateImpact = useCallback( (value: boolean) => { @@ -128,14 +87,6 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { return isVisible ? ( - {showZeroApprovalWarning && } - {showPriceImpactWarning && ( - - )} {showRateImpactWarning && ( */} {showHighFeeWarning && } - {showApprovalBundlingBanner && } - {showSafeWcBundlingBanner && } {showNativeSellWarning && } ) : null diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx index ceb55b05ff..a4a8ddf4b0 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx @@ -8,7 +8,7 @@ import { Field } from 'legacy/state/types' import { LimitOrdersWarnings } from 'modules/limitOrders/containers/LimitOrdersWarnings' import { useLimitOrdersWidgetActions } from 'modules/limitOrders/containers/LimitOrdersWidget/hooks/useLimitOrdersWidgetActions' import { TradeButtons } from 'modules/limitOrders/containers/TradeButtons' -import { TradeWidget, useTradePriceImpact } from 'modules/trade' +import { TradeWidget, TradeWidgetSlots, useTradePriceImpact } from 'modules/trade' import { useTradeConfirmState } from 'modules/trade' import { BulletListItem, UnlockWidgetScreen } from 'modules/trade/pure/UnlockWidgetScreen' import { useSetTradeQuoteParams, useTradeQuote } from 'modules/tradeQuote' @@ -79,7 +79,7 @@ export function LimitOrdersWidget() { const priceImpact = useTradePriceImpact() const quoteAmount = useMemo( () => (isSell ? inputCurrencyAmount : outputCurrencyAmount), - [isSell, inputCurrencyAmount, outputCurrencyAmount] + [isSell, inputCurrencyAmount, outputCurrencyAmount], ) useSetTradeQuoteParams(quoteAmount) @@ -136,15 +136,6 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { feeAmount, } = props - const inputCurrency = inputCurrencyInfo.currency - const outputCurrency = outputCurrencyInfo.currency - - const isTradePriceUpdating = useMemo(() => { - if (!inputCurrency || !outputCurrency) return false - - return isRateLoading - }, [isRateLoading, inputCurrency, outputCurrency]) - const tradeContext = useTradeFlowContext() const updateLimitOrdersState = useUpdateLimitOrdersRawState() const localFormValidation = useLimitOrdersFormState() @@ -164,7 +155,7 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { label: outputCurrencyInfo.label, } - const slots = { + const slots: TradeWidgetSlots = { settingsWidget: , lockScreen: isUnlocked ? undefined : ( { ), - bottomContent: ( - <> - - - - - - - - - - - ), + bottomContent(warnings) { + return ( + <> + + + + + + {warnings} + + + + + + ) + }, outerContent: <>{isUnlocked && }, } @@ -204,10 +198,11 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { compactView: false, recipient, showRecipient, - isTradePriceUpdating, + isTradePriceUpdating: isRateLoading, priceImpact, disablePriceImpact: localFormValidation === LimitOrdersFormState.FeeExceedsFrom, disableQuotePolling: isConfirmOpen, + hideTradeWarnings: !!localFormValidation, } return ( diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useLimitOrdersWarningsAccepted.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useLimitOrdersWarningsAccepted.ts index 70b7df391e..9270330b3e 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useLimitOrdersWarningsAccepted.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useLimitOrdersWarningsAccepted.ts @@ -1,16 +1,15 @@ import { useAtomValue } from 'jotai' -import { useMemo } from 'react' import { limitOrdersWarningsAtom } from 'modules/limitOrders/state/limitOrdersWarningsAtom' +import { useIsNoImpactWarningAccepted } from 'modules/trade' export function useLimitOrdersWarningsAccepted(isConfirmScreen: boolean): boolean { - const { isPriceImpactAccepted, isRateImpactAccepted } = useAtomValue(limitOrdersWarningsAtom) + const { isRateImpactAccepted } = useAtomValue(limitOrdersWarningsAtom) + const isPriceImpactAccepted = useIsNoImpactWarningAccepted() - return useMemo(() => { - if (isConfirmScreen) { - return isRateImpactAccepted - } else { - return isPriceImpactAccepted - } - }, [isConfirmScreen, isPriceImpactAccepted, isRateImpactAccepted]) + if (isConfirmScreen) { + return isRateImpactAccepted + } else { + return isPriceImpactAccepted + } } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts index 4ab16a5a0a..5e956da0d8 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts @@ -14,11 +14,11 @@ import { calculateLimitOrdersDeadline } from 'modules/limitOrders/utils/calculat import { emitPostedOrderEvent } from 'modules/orders' import { handlePermit } from 'modules/permit' import { callDataContainsPermitSigner } from 'modules/permit' -import { presignOrderStep } from 'modules/swap/services/swapFlow/steps/presignOrderStep' import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep' import { logTradeFlow } from 'modules/trade/utils/logger' import { getSwapErrorMessage } from 'modules/trade/utils/swapErrorHelper' import { TradeFlowAnalyticsContext, tradeFlowAnalytics } from 'modules/trade/utils/tradeFlowAnalytics' +import { presignOrderStep } from 'modules/tradeFlow/services/swapFlow/steps/presignOrderStep' export async function tradeFlow( params: TradeFlowContext, diff --git a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersWarningsAtom.ts b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersWarningsAtom.ts index 61a4ca8fff..51295d045e 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersWarningsAtom.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersWarningsAtom.ts @@ -2,12 +2,10 @@ import { atom } from 'jotai' interface LimitOrdersWarnings { isRateImpactAccepted: boolean - isPriceImpactAccepted: boolean } export const limitOrdersWarningsAtom = atom({ isRateImpactAccepted: false, - isPriceImpactAccepted: false, }) export const updateLimitOrdersWarningsAtom = atom(null, (get, set, nextState: Partial) => { diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/OrderTypeField.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/OrderTypeField.tsx index 48b9f0abad..d7256f6ffa 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/OrderTypeField.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/OrderTypeField.tsx @@ -14,6 +14,7 @@ const ORDER_UI_TYPE_LABELS: Record = { [UiOrderType.LIMIT]: 'Limit', [UiOrderType.TWAP]: 'TWAP', [UiOrderType.HOOKS]: 'Hooks', + [UiOrderType.YIELD]: 'Yield', } export function OrderTypeField({ order }: Props) { diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts index b837474df6..3cceff4ffd 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts @@ -23,6 +23,7 @@ const ORDER_TYPE_SUPPORTS_PERMIT: Record = { [TradeType.SWAP]: true, [TradeType.LIMIT_ORDER]: true, [TradeType.ADVANCED_ORDERS]: false, + [TradeType.YIELD]: true, } const UNSUPPORTED: PermitInfo = { type: 'unsupported', name: 'native' } diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index 9b58d26442..0403137082 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -1,44 +1,37 @@ -import { useCallback, useMemo, useState } from 'react' +import { useMemo } from 'react' import { getMinimumReceivedTooltip } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { Command } from '@cowprotocol/types' import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { Percent, TradeType } from '@uniswap/sdk-core' -import { HighFeeWarning } from 'legacy/components/SwapWarnings' import { PriceImpact } from 'legacy/hooks/usePriceImpact' -import { useOrder } from 'legacy/state/orders/hooks' import TradeGp from 'legacy/state/swap/TradeGp' +import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { useInjectedWidgetParams } from 'modules/injectedWidget' -import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' +import { useAppData } from 'modules/appData' import { TradeConfirmation, TradeConfirmModal, + useIsEoaEthFlow, + useOrderSubmittedContent, useReceiveAmountInfo, + useShouldPayGas, useTradeConfirmActions, - useTradeConfirmState, } from 'modules/trade' import { TradeBasicConfirmDetails } from 'modules/trade/containers/TradeBasicConfirmDetails' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' +import { useIsSmartSlippageApplied } from 'modules/tradeSlippage' +import { HighFeeWarning } from 'modules/tradeWidgetAddons' +import { NetworkCostsTooltipSuffix, RowDeadline } from 'modules/tradeWidgetAddons' -import { useOrderProgressBarV2Props } from 'common/hooks/orderProgressBarV2' import { CurrencyPreviewInfo } from 'common/pure/CurrencyAmountPreview' import { NetworkCostsSuffix } from 'common/pure/NetworkCostsSuffix' import { RateInfoParams } from 'common/pure/RateInfo' -import { TransactionSubmittedContent } from 'common/pure/TransactionSubmittedContent' +import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' import useNativeCurrency from 'lib/hooks/useNativeCurrency' -import { useBaseFlowContextSource } from '../../hooks/useFlowContext' -import { useIsEoaEthFlow } from '../../hooks/useIsEoaEthFlow' -import { useNavigateToNewOrderCallback } from '../../hooks/useNavigateToNewOrderCallback' -import { useShouldPayGas } from '../../hooks/useShouldPayGas' import { useSwapConfirmButtonText } from '../../hooks/useSwapConfirmButtonText' import { useSwapState } from '../../hooks/useSwapState' -import { NetworkCostsTooltipSuffix } from '../../pure/NetworkCostsTooltipSuffix' -import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from '../../pure/Row/RowSlippageContent' -import { RowDeadline } from '../Row/RowDeadline' const CONFIRM_TITLE = 'Swap' @@ -73,13 +66,11 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const { recipient } = useSwapState() const tradeConfirmActions = useTradeConfirmActions() const receiveAmountInfo = useReceiveAmountInfo() - const widgetParams = useInjectedWidgetParams() const shouldPayGas = useShouldPayGas() const isEoaEthFlow = useIsEoaEthFlow() const nativeCurrency = useNativeCurrency() - const baseFlowContextSource = useBaseFlowContextSource() - - const isInvertedState = useState(false) + const appData = useAppData() + const [userDeadline] = useUserTransactionTTL() const slippageAdjustedSellAmount = trade?.maximumAmountIn(allowedSlippage) const isExactIn = trade?.tradeType === TradeType.EXACT_INPUT @@ -103,10 +94,10 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { networkCostsSuffix: shouldPayGas ? : null, networkCostsTooltipSuffix: , }), - [chainId, allowedSlippage, nativeCurrency.symbol, isEoaEthFlow, isExactIn, shouldPayGas], + [chainId, allowedSlippage, nativeCurrency.symbol, isEoaEthFlow, isExactIn, shouldPayGas, isSmartSlippageApplied], ) - const submittedContent = useSubmittedContent(chainId) + const submittedContent = useOrderSubmittedContent(chainId) return ( @@ -123,17 +114,15 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { priceImpact={priceImpact} buttonText={buttonText} recipient={recipient} - appData={baseFlowContextSource?.appData || undefined} + appData={appData || undefined} > {(restContent) => ( <> {receiveAmountInfo && ( - + )} {restContent} - - {!priceImpact.priceImpact && } + )} ) } - -function useSubmittedContent(chainId: SupportedChainId) { - const { transactionHash } = useTradeConfirmState() - const order = useOrder({ chainId, id: transactionHash || undefined }) - - const orderProgressBarV2Props = useOrderProgressBarV2Props(chainId, order) - - const navigateToNewOrderCallback = useNavigateToNewOrderCallback() - - return useCallback( - (onDismiss: Command) => ( - - ), - [chainId, transactionHash, orderProgressBarV2Props, navigateToNewOrderCallback], - ) -} diff --git a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowDeadline/index.tsx deleted file mode 100644 index dc1025aeb8..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowDeadline/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useMemo } from 'react' - -import { useToggleSettingsMenu } from 'legacy/state/application/hooks' -import { useUserTransactionTTL } from 'legacy/state/user/hooks' - -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' -import { RowDeadlineContent } from 'modules/swap/pure/Row/RowDeadline' -import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap' - -import useNativeCurrency from 'lib/hooks/useNativeCurrency' - -export function RowDeadline() { - const [userDeadline] = useUserTransactionTTL() - const toggleSettings = useToggleSettingsMenu() - const isEoaEthFlow = useIsEoaEthFlow() - const nativeCurrency = useNativeCurrency() - const isWrapOrUnwrap = useIsWrapOrUnwrap() - - const props = useMemo(() => { - const displayDeadline = Math.floor(userDeadline / 60) + ' minutes' - return { - userDeadline, - symbols: [nativeCurrency.symbol], - displayDeadline, - isEoaEthFlow, - isWrapOrUnwrap, - toggleSettings, - showSettingOnClick: true, - } - }, [isEoaEthFlow, isWrapOrUnwrap, nativeCurrency.symbol, toggleSettings, userDeadline]) - - if (!isEoaEthFlow || isWrapOrUnwrap) { - return null - } - - return -} diff --git a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx deleted file mode 100644 index 812b97be9e..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useMemo } from 'react' - -import { formatPercent } from '@cowprotocol/common-utils' -import { useWalletInfo } from '@cowprotocol/wallet' -import { Percent } from '@uniswap/sdk-core' - -import { useToggleSettingsMenu } from 'legacy/state/application/hooks' - -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' -import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' -import { useSetSlippage } from 'modules/swap/hooks/useSetSlippage' -import { useSmartSwapSlippage } from 'modules/swap/hooks/useSwapSlippage' -import { useTradePricesUpdate } from 'modules/swap/hooks/useTradePricesUpdate' -import { RowSlippageContent } from 'modules/swap/pure/Row/RowSlippageContent' - -import useNativeCurrency from 'lib/hooks/useNativeCurrency' - -export interface RowSlippageProps { - allowedSlippage: Percent - showSettingOnClick?: boolean - slippageLabel?: React.ReactNode - slippageTooltip?: React.ReactNode - isSlippageModified: boolean -} - -export function RowSlippage({ - allowedSlippage, - showSettingOnClick = true, - slippageTooltip, - slippageLabel, - isSlippageModified, -}: RowSlippageProps) { - const { chainId } = useWalletInfo() - const toggleSettings = useToggleSettingsMenu() - - const isEoaEthFlow = useIsEoaEthFlow() - const nativeCurrency = useNativeCurrency() - const smartSwapSlippage = useSmartSwapSlippage() - const isSmartSlippageApplied = useIsSmartSlippageApplied() - const setSlippage = useSetSlippage() - const isTradePriceUpdating = useTradePricesUpdate() - - const props = useMemo( - () => ({ - chainId, - isEoaEthFlow, - symbols: [nativeCurrency.symbol], - showSettingOnClick, - allowedSlippage, - slippageLabel, - slippageTooltip, - displaySlippage: `${formatPercent(allowedSlippage)}%`, - isSmartSlippageApplied, - isSmartSlippageLoading: isTradePriceUpdating, - smartSlippage: smartSwapSlippage && !isEoaEthFlow ? `${formatPercent(new Percent(smartSwapSlippage, 10_000))}%` : undefined, - setAutoSlippage: smartSwapSlippage && !isEoaEthFlow ? () => setSlippage(null) : undefined, - }), - [chainId, isEoaEthFlow, nativeCurrency.symbol, showSettingOnClick, allowedSlippage, slippageLabel, slippageTooltip, smartSwapSlippage, isSmartSlippageApplied, isTradePriceUpdating] - ) - - return -} diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SurplusModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SurplusModalSetup/index.tsx index ec16c85a42..d53df031ed 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SurplusModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SurplusModalSetup/index.tsx @@ -4,13 +4,12 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { useOrder } from 'legacy/state/orders/hooks' -import { useNavigateToNewOrderCallback } from 'modules/swap/hooks/useNavigateToNewOrderCallback' +import { useTradeConfirmState, useNavigateToNewOrderCallback } from 'modules/trade' import { useOrderProgressBarV2Props } from 'common/hooks/orderProgressBarV2' import { CowModal } from 'common/pure/Modal' import { TransactionSubmittedContent } from 'common/pure/TransactionSubmittedContent' -import { useTradeConfirmState } from '../../../trade' import { useOrderIdForSurplusModal, useRemoveOrderFromSurplusQueue } from '../../state/surplusModal' // TODO: rename? diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx index 021a8fd42e..3e3a8fcd8e 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx @@ -1,16 +1,13 @@ import { percentToBps } from '@cowprotocol/common-utils' -import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' +import { AppDataUpdater } from 'modules/appData' +import { useTradeSlippage, useIsSmartSlippageApplied } from 'modules/tradeSlippage' -import { AppDataUpdater } from '../../../appData' -import { useSwapSlippage } from '../../hooks/useSwapSlippage' -import { BaseFlowContextUpdater } from '../../updaters/BaseFlowContextUpdater' -import { SmartSlippageUpdater } from '../../updaters/SmartSlippageUpdater' import { SwapAmountsFromUrlUpdater } from '../../updaters/SwapAmountsFromUrlUpdater' import { SwapDerivedStateUpdater } from '../../updaters/SwapDerivedStateUpdater' export function SwapUpdaters() { - const slippage = useSwapSlippage() + const slippage = useTradeSlippage() const isSmartSlippageApplied = useIsSmartSlippageApplied() return ( @@ -22,8 +19,6 @@ export function SwapUpdaters() { /> - - ) } diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx index b9db3140fb..30c61ac4bc 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -2,22 +2,19 @@ import { ReactNode, useCallback, useMemo, useState } from 'react' import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCIES, TokenWithLogo } from '@cowprotocol/common-const' -import { isFractionFalsy, percentToBps } from '@cowprotocol/common-utils' import { useIsTradeUnsupported } from '@cowprotocol/tokens' -import { useIsSafeViaWc, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' +import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { TradeType } from '@cowprotocol/widget-lib' import { NetworkAlert } from 'legacy/components/NetworkAlert/NetworkAlert' -import { SettingsTab } from 'legacy/components/Settings' import { useModalIsOpen } from 'legacy/state/application/hooks' import { ApplicationModal } from 'legacy/state/application/reducer' import { Field } from 'legacy/state/types' +import { useRecipientToggleManager, useUserTransactionTTL } from 'legacy/state/user/hooks' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { EthFlowModal, EthFlowProps } from 'modules/swap/containers/EthFlow' import { SwapModals, SwapModalsProps } from 'modules/swap/containers/SwapModals' -import { SwapButtonState } from 'modules/swap/helpers/getSwapButtonState' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useShowRecipientControls } from 'modules/swap/hooks/useShowRecipientControls' import { useSwapButtonContext } from 'modules/swap/hooks/useSwapButtonContext' import { useSwapCurrenciesAmounts } from 'modules/swap/hooks/useSwapCurrenciesAmounts' @@ -29,36 +26,32 @@ import { SwapWarningsTop, SwapWarningsTopProps, } from 'modules/swap/pure/warnings' -import { TradeWidget, TradeWidgetContainer, useReceiveAmountInfo, useTradePriceImpact } from 'modules/trade' -import { useTradeRouteContext } from 'modules/trade/hooks/useTradeRouteContext' -import { useWrappedToken } from 'modules/trade/hooks/useWrappedToken' +import { + TradeWidget, + TradeWidgetContainer, + TradeWidgetSlots, + useReceiveAmountInfo, + useTradePriceImpact, +} from 'modules/trade' +import { + useIsEoaEthFlow, + useTradeRouteContext, + useUnknownImpactWarning, + useIsNoImpactWarningAccepted, +} from 'modules/trade' import { getQuoteTimeOffset } from 'modules/tradeQuote' +import { useTradeSlippage } from 'modules/tradeSlippage' +import { SettingsTab, TradeRateDetails, useHighFeeWarning } from 'modules/tradeWidgetAddons' import { useTradeUsdAmounts } from 'modules/usdAmount' -import { useShouldZeroApprove } from 'modules/zeroApproval' import { useSetLocalTimeOffset } from 'common/containers/InvalidLocalTimeWarning/localTimeOffsetState' import { useRateInfoParams } from 'common/hooks/useRateInfoParams' import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' import { SWAP_QUOTE_CHECK_INTERVAL } from 'common/updaters/FeesUpdater' -import useNativeCurrency from 'lib/hooks/useNativeCurrency' -import { useIsSlippageModified } from '../../hooks/useIsSlippageModified' -import { useIsSmartSlippageApplied } from '../../hooks/useIsSmartSlippageApplied' -import { useIsSwapEth } from '../../hooks/useIsSwapEth' -import { useSwapSlippage } from '../../hooks/useSwapSlippage' -import { - useDerivedSwapInfo, - useHighFeeWarning, - useSwapActionHandlers, - useSwapState, - useUnknownImpactWarning, -} from '../../hooks/useSwapState' +import { useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../hooks/useSwapState' import { useTradeQuoteStateFromLegacy } from '../../hooks/useTradeQuoteStateFromLegacy' import { ConfirmSwapModalSetup } from '../ConfirmSwapModalSetup' -import { TradeRateDetails } from '../TradeRateDetails' - -const BUTTON_STATES_TO_SHOW_BUNDLE_APPROVAL_BANNER = [SwapButtonState.ApproveAndSwap] -const BUTTON_STATES_TO_SHOW_BUNDLE_WRAP_BANNER = [SwapButtonState.WrapAndSwap] export interface SwapWidgetProps { topContent?: ReactNode @@ -66,10 +59,9 @@ export interface SwapWidgetProps { } export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { - const { chainId, account } = useWalletInfo() - const { slippageAdjustedSellAmount, currencies, trade } = useDerivedSwapInfo() - const slippage = useSwapSlippage() - const isSlippageModified = useIsSlippageModified() + const { chainId } = useWalletInfo() + const { currencies, trade } = useDerivedSwapInfo() + const slippage = useTradeSlippage() const parsedAmounts = useSwapCurrenciesAmounts() const { isSupportedWallet } = useWalletDetails() const isSwapUnsupported = useIsTradeUnsupported(currencies.INPUT, currencies.OUTPUT) @@ -78,12 +70,13 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const { independentField, recipient } = swapState const showRecipientControls = useShowRecipientControls(recipient) const isEoaEthFlow = useIsEoaEthFlow() - const shouldZeroApprove = useShouldZeroApprove(slippageAdjustedSellAmount) const widgetParams = useInjectedWidgetParams() - const { enabledTradeTypes, banners: widgetBanners } = widgetParams + const { enabledTradeTypes } = widgetParams const priceImpactParams = useTradePriceImpact() const tradeQuoteStateOverride = useTradeQuoteStateFromLegacy() const receiveAmountInfo = useReceiveAmountInfo() + const recipientToggleState = useRecipientToggleManager() + const deadlineState = useUserTransactionTTL() const isTradePriceUpdating = useTradePricesUpdate() @@ -163,17 +156,10 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const [showNativeWrapModal, setOpenNativeWrapModal] = useState(false) const showCowSubsidyModal = useModalIsOpen(ApplicationModal.COW_SUBSIDY) - // Hide the price impact warning when there is priceImpact value or when it's loading - // The loading values is debounced in useFiatValuePriceImpact() to avoid flickering - const hideUnknownImpactWarning = - isFractionFalsy(parsedAmounts.INPUT) || - isFractionFalsy(parsedAmounts.OUTPUT) || - !!priceImpactParams.priceImpact || - priceImpactParams.loading - - const { feeWarningAccepted, setFeeWarningAccepted } = useHighFeeWarning(trade) - const { impactWarningAccepted: _impactWarningAccepted, setImpactWarningAccepted } = useUnknownImpactWarning() - const impactWarningAccepted = hideUnknownImpactWarning || _impactWarningAccepted + const { feeWarningAccepted } = useHighFeeWarning() + const noImpactWarningAccepted = useIsNoImpactWarningAccepted() + const { impactWarningAccepted: unknownImpactWarning } = useUnknownImpactWarning() + const impactWarningAccepted = noImpactWarningAccepted || unknownImpactWarning const openNativeWrapModal = useCallback(() => setOpenNativeWrapModal(true), []) const dismissNativeWrapModal = useCallback(() => setOpenNativeWrapModal(false), []) @@ -182,7 +168,6 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { feeWarningAccepted, impactWarningAccepted, openNativeWrapModal, - priceImpactParams, }) const tradeUrlParams = useTradeRouteContext() @@ -201,52 +186,15 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { showNativeWrapModal, showCowSubsidyModal, } - - const showApprovalBundlingBanner = BUTTON_STATES_TO_SHOW_BUNDLE_APPROVAL_BANNER.includes( - swapButtonContext.swapButtonState, - ) - const showWrapBundlingBanner = BUTTON_STATES_TO_SHOW_BUNDLE_WRAP_BANNER.includes(swapButtonContext.swapButtonState) - - const isSafeViaWc = useIsSafeViaWc() - const isSwapEth = useIsSwapEth() - - const showSafeWcApprovalBundlingBanner = - !showApprovalBundlingBanner && isSafeViaWc && swapButtonContext.swapButtonState === SwapButtonState.NeedApprove - - const showSafeWcWrapBundlingBanner = !showWrapBundlingBanner && isSafeViaWc && isSwapEth - - // Show the same banner when approval is needed or selling native token - const showSafeWcBundlingBanner = - (showSafeWcApprovalBundlingBanner || showSafeWcWrapBundlingBanner) && !widgetBanners?.hideSafeWebAppBanner - const showTwapSuggestionBanner = !enabledTradeTypes || enabledTradeTypes.includes(TradeType.ADVANCED) - const nativeCurrencySymbol = useNativeCurrency().symbol || 'ETH' - const wrappedCurrencySymbol = useWrappedToken().symbol || 'WETH' - - const isSuggestedSlippage = useIsSmartSlippageApplied() && !isTradePriceUpdating && !!account - const swapWarningsTopProps: SwapWarningsTopProps = { chainId, trade, - account, - feeWarningAccepted, - impactWarningAccepted, - hideUnknownImpactWarning, - showApprovalBundlingBanner, - showWrapBundlingBanner, - showSafeWcBundlingBanner, showTwapSuggestionBanner, - nativeCurrencySymbol, - wrappedCurrencySymbol, - setFeeWarningAccepted, - setImpactWarningAccepted, - shouldZeroApprove, buyingFiatAmount, priceImpact: priceImpactParams.priceImpact, tradeUrlParams, - slippageBps: percentToBps(slippage), - isSuggestedSlippage, } const swapWarningsBottomProps: SwapWarningsBottomProps = { @@ -256,29 +204,43 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { currencyOut: currencies.OUTPUT || undefined, } - const slots = { - settingsWidget: , + const slots: TradeWidgetSlots = { + settingsWidget: , topContent, - bottomContent: ( - <> - {bottomContent} - - - - - + bottomContent: useCallback( + (warnings: ReactNode | null) => { + return ( + <> + {bottomContent} + + + {warnings} + + + + ) + }, + [ + bottomContent, + deadlineState, + isTradePriceUpdating, + rateInfoParams, + swapButtonContext, + swapWarningsTopProps, + swapWarningsBottomProps, + ], ), } const params = { isEoaEthFlow, compactView: true, + enableSmartSlippage: true, recipient, showRecipient: showRecipientControls, isTradePriceUpdating, diff --git a/apps/cowswap-frontend/src/modules/swap/helpers/getEthFlowEnabled.ts b/apps/cowswap-frontend/src/modules/swap/helpers/getEthFlowEnabled.ts deleted file mode 100644 index 76e9a374e4..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/helpers/getEthFlowEnabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getEthFlowEnabled(isSmartContractWallet: boolean): boolean { - return !isSmartContractWallet -} diff --git a/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts b/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts index 34f8a208aa..677617b953 100644 --- a/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts +++ b/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts @@ -4,7 +4,6 @@ import { QuoteError } from 'legacy/state/price/actions' import { QuoteInformationObject } from 'legacy/state/price/reducer' import TradeGp from 'legacy/state/swap/TradeGp' -import { getEthFlowEnabled } from 'modules/swap/helpers/getEthFlowEnabled' import { isQuoteExpired, QuoteDeadlineParams } from 'modules/tradeQuote' import { ApprovalState } from 'common/hooks/useApproveState' @@ -141,7 +140,7 @@ export function getSwapButtonState(input: SwapButtonStateParams): SwapButtonStat } if (input.isNativeIn) { - if (getEthFlowEnabled(input.isSmartContractWallet === true)) { + if (!input.isSmartContractWallet) { return SwapButtonState.RegularEthFlowSwap } else if (input.isBundlingSupported) { return SwapButtonState.WrapAndSwap diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts deleted file mode 100644 index 6bed3d4da2..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useMemo } from 'react' - -import { getWrappedToken } from '@cowprotocol/common-utils' -import { OrderKind } from '@cowprotocol/cow-sdk' -import { useSafeAppsSdk } from '@cowprotocol/wallet' -import { useWalletProvider } from '@cowprotocol/wallet-provider' -import { TradeType } from '@uniswap/sdk-core' - -import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext' -import { BaseSafeFlowContext } from 'modules/swap/services/types' - -import { useGP2SettlementContract } from 'common/hooks/useContract' -import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress' - -export function useBaseSafeBundleFlowContext(): BaseSafeFlowContext | null { - const baseProps = useBaseFlowContextSource() - const sellToken = baseProps?.trade ? getWrappedToken(baseProps.trade.inputAmount.currency) : undefined - const settlementContract = useGP2SettlementContract() - const spender = useTradeSpenderAddress() - - const safeAppsSdk = useSafeAppsSdk() - const provider = useWalletProvider() - - return useMemo(() => { - if (!baseProps?.trade || !settlementContract || !spender || !safeAppsSdk || !provider) return null - - const baseContext = getFlowContext({ - baseProps, - sellToken, - kind: baseProps.trade.tradeType === TradeType.EXACT_INPUT ? OrderKind.SELL : OrderKind.BUY, - }) - - if (!baseContext) return null - - return { - ...baseContext, - settlementContract, - spender, - safeAppsSdk, - provider, - } - }, [baseProps, settlementContract, spender, safeAppsSdk, provider, sellToken]) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts index c3939fb295..f73262fc99 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts @@ -1,52 +1,54 @@ import { useSetAtom } from 'jotai' -import { useMemo } from 'react' -import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' -import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' + +import useSWR from 'swr' import { useTransactionAdder } from 'legacy/state/enhancedTransactions/hooks' +import { useGetQuoteAndStatus } from 'legacy/state/price/hooks' -import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext' -import { EthFlowContext } from 'modules/swap/services/types' -import { addInFlightOrderIdAtom } from 'modules/swap/state/EthFlow/ethFlowInFlightOrderIdsAtom' +import { useAppData, useUploadAppData } from 'modules/appData' import { useEthFlowContract } from 'common/hooks/useContract' import { useCheckEthFlowOrderExists } from './useCheckEthFlowOrderExists' +import { useDerivedSwapInfo } from './useSwapState' -import { FlowType } from '../types/flowContext' +import { EthFlowContext } from '../services/types' +import { addInFlightOrderIdAtom } from '../state/EthFlow/ethFlowInFlightOrderIdsAtom' export function useEthFlowContext(): EthFlowContext | null { + const { chainId } = useWalletInfo() + const { currenciesIds } = useDerivedSwapInfo() + const { quote } = useGetQuoteAndStatus({ + token: currenciesIds.INPUT, + chainId, + }) const contract = useEthFlowContract() - const baseProps = useBaseFlowContextSource() const addTransaction = useTransactionAdder() - - const sellToken = baseProps?.chainId ? NATIVE_CURRENCIES[baseProps.chainId as SupportedChainId] : undefined + const uploadAppData = useUploadAppData() + const appData = useAppData() const addInFlightOrderId = useSetAtom(addInFlightOrderIdAtom) const checkEthFlowOrderExists = useCheckEthFlowOrderExists() - const baseContext = useMemo( - () => - baseProps && - getFlowContext({ - baseProps, - sellToken, - kind: OrderKind.SELL, - }), - [baseProps, sellToken], + return ( + useSWR( + appData && contract + ? [quote, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, uploadAppData, appData] + : null, + ([quote, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, uploadAppData, appData]) => { + return { + quote, + contract, + addTransaction, + checkEthFlowOrderExists, + addInFlightOrderId, + uploadAppData, + appData, + } + }, + ).data || null ) - - return useMemo(() => { - if (!baseContext || !contract || baseProps?.flowType !== FlowType.EOA_ETH_FLOW) return null - - return { - ...baseContext, - contract, - addTransaction, - checkEthFlowOrderExists, - addInFlightOrderId, - } - }, [baseContext, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, baseProps?.flowType]) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts deleted file mode 100644 index 9510a7c33d..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { useAtomValue } from 'jotai/index' - -import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' -import { getIsNativeToken } from '@cowprotocol/common-utils' -import { OrderClass, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' -import { UiOrderType } from '@cowprotocol/types' -import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' - -import { computeSlippageAdjustedAmounts } from 'legacy/utils/prices' -import { PostOrderParams } from 'legacy/utils/trade' - -import { BaseFlowContext } from 'modules/swap/services/types' -import { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics' -import { getOrderValidTo } from 'modules/tradeQuote' - -import { useSafeMemo } from 'common/hooks/useSafeMemo' - -import { useSwapSlippage } from './useSwapSlippage' -import { useDerivedSwapInfo } from './useSwapState' - -import { getAmountsForSignature } from '../helpers/getAmountsForSignature' -import { baseFlowContextSourceAtom } from '../state/baseFlowContextSourceAtom' -import { BaseFlowContextSource } from '../types/flowContext' - -export function useSwapAmountsWithSlippage(): [ - CurrencyAmount | undefined, - CurrencyAmount | undefined, -] { - const slippage = useSwapSlippage() - const { trade } = useDerivedSwapInfo() - - const { INPUT, OUTPUT } = computeSlippageAdjustedAmounts(trade, slippage) - - return useSafeMemo(() => [INPUT, OUTPUT], [INPUT, OUTPUT]) -} - -export function useBaseFlowContextSource(): BaseFlowContextSource | null { - return useAtomValue(baseFlowContextSourceAtom) -} - -type BaseGetFlowContextProps = { - baseProps: BaseFlowContextSource - sellToken?: Token - kind: OrderKind -} - -export function getFlowContext({ baseProps, sellToken, kind }: BaseGetFlowContextProps): BaseFlowContext | null { - const { - chainId, - account, - provider, - trade, - appData, - wethContract, - inputAmountWithSlippage, - outputAmountWithSlippage, - gnosisSafeInfo, - recipient, - recipientAddressOrName, - deadline, - ensRecipientAddress, - allowsOffchainSigning, - closeModals, - addOrderCallback, - uploadAppData, - dispatch, - flowType, - sellTokenContract, - allowedSlippage, - tradeConfirmActions, - getCachedPermit, - quote, - typedHooks, - } = baseProps - - if ( - !chainId || - !account || - !provider || - !trade || - !appData || - !wethContract || - !inputAmountWithSlippage || - !outputAmountWithSlippage - ) { - return null - } - - const isSafeWallet = !!gnosisSafeInfo - - const buyToken = getIsNativeToken(trade.outputAmount.currency) - ? NATIVE_CURRENCIES[chainId as SupportedChainId] - : trade.outputAmount.currency - const marketLabel = [sellToken?.symbol, buyToken.symbol].join(',') - - if (!sellToken || !buyToken) { - return null - } - - const swapFlowAnalyticsContext: TradeFlowAnalyticsContext = { - account, - recipient, - recipientAddress: recipientAddressOrName, - marketLabel, - orderType: UiOrderType.SWAP, - } - - const validTo = getOrderValidTo(deadline, { - validFor: quote?.validFor, - quoteValidTo: quote?.quoteValidTo, - localQuoteTimestamp: quote?.localQuoteTimestamp, - }) - - const amountsForSignature = getAmountsForSignature({ - trade, - kind, - allowedSlippage, - }) - - const orderParams: PostOrderParams = { - class: OrderClass.MARKET, - kind, - account, - chainId, - ...amountsForSignature, - sellAmountBeforeFee: trade.inputAmountWithoutFee, - feeAmount: trade.fee.feeAsCurrency, - buyToken, - sellToken, - validTo, - recipient: ensRecipientAddress || recipient || account, - recipientAddressOrName, - signer: provider.getSigner(), - allowsOffchainSigning, - partiallyFillable: false, // SWAP orders are always fill or kill - for now - appData, - quoteId: trade.quoteId, - isSafeWallet, - } - - return { - context: { - chainId, - trade, - inputAmountWithSlippage, - outputAmountWithSlippage, - flowType, - }, - flags: { - allowsOffchainSigning, - }, - callbacks: { - closeModals, - addOrderCallback, - uploadAppData, - getCachedPermit, - }, - dispatch, - swapFlowAnalyticsContext, - orderParams, - appDataInfo: appData, - sellTokenContract, - tradeConfirmActions, - quote, - typedHooks, - } -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.test.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.test.tsx deleted file mode 100644 index 4aa2350231..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { PropsWithChildren } from 'react' - -import { renderHook } from '@testing-library/react-hooks' - -import { PriceImpact } from 'legacy/hooks/usePriceImpact' -import { Field } from 'legacy/state/types' - -import { useSafeBundleApprovalFlowContext } from 'modules/swap/hooks/useSafeBundleApprovalFlowContext' -import { ethFlow } from 'modules/swap/services/ethFlow' -import { safeBundleApprovalFlow, safeBundleEthFlow } from 'modules/swap/services/safeBundleFlow' -import { swapFlow } from 'modules/swap/services/swapFlow' - -import { WithModalProvider } from 'utils/withModalProvider' - -import { useEthFlowContext } from './useEthFlowContext' -import { useHandleSwap } from './useHandleSwap' -import { useSafeBundleEthFlowContext } from './useSafeBundleEthFlowContext' -import { useSwapFlowContext } from './useSwapFlowContext' -import { useSwapActionHandlers } from './useSwapState' - -jest.mock('./useSwapState') -jest.mock('./useSwapFlowContext') -jest.mock('./useEthFlowContext') -jest.mock('./useSafeBundleApprovalFlowContext') -jest.mock('./useSafeBundleEthFlowContext') -jest.mock('modules/swap/services/swapFlow') -jest.mock('modules/swap/services/ethFlow') -jest.mock('modules/swap/services/safeBundleFlow') -jest.mock('modules/twap/state/twapOrdersListAtom', () => ({})) -jest.mock('modules/analytics/useAnalyticsReporterCowSwap') - -const mockUseSwapActionHandlers = useSwapActionHandlers as jest.MockedFunction -const mockSwapFlow = swapFlow as jest.MockedFunction -const mockEthFlow = ethFlow as jest.MockedFunction -const mockSafeBundleApprovalFlow = safeBundleApprovalFlow as jest.MockedFunction -const mockSafeBundleEthFlow = safeBundleEthFlow as jest.MockedFunction -const mockUseSwapFlowContext = useSwapFlowContext as jest.MockedFunction -const mockUseEthFlowContext = useEthFlowContext as jest.MockedFunction -const mockUseSafeBundleFlowContext = useSafeBundleApprovalFlowContext as jest.MockedFunction< - typeof useSafeBundleApprovalFlowContext -> -const mockUseSafeBundleEthFlowContext = useSafeBundleEthFlowContext as jest.MockedFunction< - typeof useSafeBundleEthFlowContext -> - -const priceImpactMock: PriceImpact = { - priceImpact: undefined, - loading: false, -} - -const WithProviders = ({ children }: PropsWithChildren) => { - return {children} -} - -describe('useHandleSwapCallback', () => { - let onUserInput: jest.Mock - let onChangeRecipient: jest.Mock - - beforeEach(() => { - onChangeRecipient = jest.fn() - onUserInput = jest.fn() - - mockUseSwapActionHandlers.mockReturnValue({ onChangeRecipient, onUserInput } as any) - mockUseSwapFlowContext.mockReturnValue(1 as any) - mockUseEthFlowContext.mockReturnValue(1 as any) - mockUseSafeBundleFlowContext.mockReturnValue(1 as any) - mockUseSafeBundleEthFlowContext.mockReturnValue(1 as any) - - mockSwapFlow.mockImplementation(() => Promise.resolve()) - mockEthFlow.mockImplementation(() => Promise.resolve()) - mockSafeBundleApprovalFlow.mockImplementation(() => Promise.resolve()) - mockSafeBundleEthFlow.mockImplementation(() => Promise.resolve()) - }) - - it('When a swap happened, then the recipient value should be deleted', async () => { - const { result } = renderHook(() => useHandleSwap(priceImpactMock), { wrapper: WithProviders }) - - await result.current() - - expect(onChangeRecipient).toBeCalledTimes(1) - expect(onChangeRecipient).toHaveBeenCalledWith(null) - expect(onUserInput).toBeCalledTimes(1) - expect(onUserInput).toHaveBeenCalledWith(Field.INPUT, '') - }) -}) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.ts deleted file mode 100644 index b345a36269..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useCallback } from 'react' - -import { PriceImpact } from 'legacy/hooks/usePriceImpact' -import { Field } from 'legacy/state/types' - -import { useSafeBundleApprovalFlowContext } from 'modules/swap/hooks/useSafeBundleApprovalFlowContext' -import { ethFlow } from 'modules/swap/services/ethFlow' -import { safeBundleApprovalFlow, safeBundleEthFlow } from 'modules/swap/services/safeBundleFlow' -import { swapFlow } from 'modules/swap/services/swapFlow' -import { logTradeFlow } from 'modules/trade/utils/logger' - -import { useConfirmPriceImpactWithoutFee } from 'common/hooks/useConfirmPriceImpactWithoutFee' - -import { useEthFlowContext } from './useEthFlowContext' -import { useSafeBundleEthFlowContext } from './useSafeBundleEthFlowContext' -import { useSwapFlowContext } from './useSwapFlowContext' -import { useSwapActionHandlers } from './useSwapState' - -export function useHandleSwap(priceImpactParams: PriceImpact): () => Promise { - const swapFlowContext = useSwapFlowContext() - const ethFlowContext = useEthFlowContext() - const safeBundleApprovalFlowContext = useSafeBundleApprovalFlowContext() - const safeBundleEthFlowContext = useSafeBundleEthFlowContext() - const { confirmPriceImpactWithoutFee } = useConfirmPriceImpactWithoutFee() - const { onChangeRecipient, onUserInput } = useSwapActionHandlers() - - return useCallback(async () => { - if (!swapFlowContext && !ethFlowContext && !safeBundleApprovalFlowContext && !safeBundleEthFlowContext) return - - const tradeResult = await (async () => { - if (safeBundleApprovalFlowContext) { - logTradeFlow('SAFE BUNDLE APPROVAL FLOW', 'Start safe bundle approval flow') - return safeBundleApprovalFlow(safeBundleApprovalFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - } else if (safeBundleEthFlowContext) { - logTradeFlow('SAFE BUNDLE ETH FLOW', 'Start safe bundle eth flow') - return safeBundleEthFlow(safeBundleEthFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - } else if (swapFlowContext) { - logTradeFlow('SWAP FLOW', 'Start swap flow') - return swapFlow(swapFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - } else if (ethFlowContext) { - logTradeFlow('ETH FLOW', 'Start eth flow') - return ethFlow(ethFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - } - })() - - const isPriceImpactDeclined = tradeResult === false - - // Clean up form fields after successful swap - if (!isPriceImpactDeclined) { - onChangeRecipient(null) - onUserInput(Field.INPUT, '') - } - }, [ - swapFlowContext, - ethFlowContext, - safeBundleApprovalFlowContext, - safeBundleEthFlowContext, - onChangeRecipient, - onUserInput, - priceImpactParams, - confirmPriceImpactWithoutFee, - ]) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts new file mode 100644 index 0000000000..10ecb4104b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts @@ -0,0 +1,42 @@ +import { useCallback } from 'react' + +import { useUserTransactionTTL } from 'legacy/state/user/hooks' + +import { useTradePriceImpact } from 'modules/trade' +import { logTradeFlow } from 'modules/trade/utils/logger' +import { useHandleSwap, useTradeFlowType } from 'modules/tradeFlow' +import { FlowType } from 'modules/tradeFlow' + +import { useConfirmPriceImpactWithoutFee } from 'common/hooks/useConfirmPriceImpactWithoutFee' +import { useSafeMemoObject } from 'common/hooks/useSafeMemo' + +import { useEthFlowContext } from './useEthFlowContext' +import { useSwapFlowContext } from './useSwapFlowContext' + +import { ethFlow } from '../services/ethFlow' + +export function useHandleSwapOrEthFlow() { + const priceImpactParams = useTradePriceImpact() + const swapFlowContext = useSwapFlowContext() + const ethFlowContext = useEthFlowContext() + const tradeFlowType = useTradeFlowType() + const { confirmPriceImpactWithoutFee } = useConfirmPriceImpactWithoutFee() + + const [deadline] = useUserTransactionTTL() + const { callback: handleSwap, contextIsReady } = useHandleSwap(useSafeMemoObject({ deadline })) + + const callback = useCallback(() => { + if (!swapFlowContext) return + + if (tradeFlowType === FlowType.EOA_ETH_FLOW) { + if (!ethFlowContext) throw new Error('Eth flow context is not ready') + + logTradeFlow('ETH FLOW', 'Start eth flow') + return ethFlow(swapFlowContext, ethFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) + } + + return handleSwap() + }, [swapFlowContext, ethFlowContext, handleSwap, tradeFlowType, priceImpactParams, confirmPriceImpactWithoutFee]) + + return { callback, contextIsReady } +} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts deleted file mode 100644 index 6f342d3120..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMemo } from 'react' - -import { getWrappedToken } from '@cowprotocol/common-utils' - -import { SafeBundleApprovalFlowContext } from 'modules/swap/services/types' - -import { useTokenContract } from 'common/hooks/useContract' - -import { useBaseSafeBundleFlowContext } from './useBaseSafeBundleFlowContext' - -import { FlowType } from '../types/flowContext' - -export function useSafeBundleApprovalFlowContext(): SafeBundleApprovalFlowContext | null { - const baseContext = useBaseSafeBundleFlowContext() - const trade = baseContext?.context.trade - - const sellToken = trade ? getWrappedToken(trade.inputAmount.currency) : undefined - const erc20Contract = useTokenContract(sellToken?.address) - - return useMemo(() => { - if ( - !baseContext || - !baseContext.context.trade || - !erc20Contract || - baseContext.context.flowType !== FlowType.SAFE_BUNDLE_APPROVAL - ) { - return null - } - - return { - ...baseContext, - erc20Contract, - } - }, [baseContext, erc20Contract]) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts deleted file mode 100644 index cc8bfb1a15..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useMemo } from 'react' - -import { SafeBundleEthFlowContext } from 'modules/swap/services/types' - -import { useWETHContract } from 'common/hooks/useContract' -import { useNeedsApproval } from 'common/hooks/useNeedsApproval' - -import { useBaseSafeBundleFlowContext } from './useBaseSafeBundleFlowContext' - -import { FlowType } from '../types/flowContext' - -export function useSafeBundleEthFlowContext(): SafeBundleEthFlowContext | null { - const baseContext = useBaseSafeBundleFlowContext() - - const wrappedNativeContract = useWETHContract() - const needsApproval = useNeedsApproval(baseContext?.context.inputAmountWithSlippage) - - return useMemo(() => { - if (!wrappedNativeContract || !baseContext || baseContext.context.flowType !== FlowType.SAFE_BUNDLE_ETH) return null - - return { - ...baseContext, - wrappedNativeContract, - needsApproval, - } - }, [baseContext, wrappedNativeContract, needsApproval]) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSetSlippage.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSetSlippage.ts deleted file mode 100644 index 579dbed4d0..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSetSlippage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useSetAtom } from 'jotai' - -import { setSwapSlippageAtom } from '../state/slippageValueAndTypeAtom' - -export function useSetSlippage() { - return useSetAtom(setSwapSlippageAtom) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index f92a26c36a..5efc07fdac 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -12,7 +12,6 @@ import { } from '@cowprotocol/wallet' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { PriceImpact } from 'legacy/hooks/usePriceImpact' import { useToggleWalletModal } from 'legacy/state/application/hooks' import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/hooks' import { Field } from 'legacy/state/types' @@ -20,10 +19,6 @@ import { Field } from 'legacy/state/types' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useTokenSupportsPermit } from 'modules/permit' import { getSwapButtonState } from 'modules/swap/helpers/getSwapButtonState' -import { useEthFlowContext } from 'modules/swap/hooks/useEthFlowContext' -import { useHandleSwap } from 'modules/swap/hooks/useHandleSwap' -import { useSafeBundleApprovalFlowContext } from 'modules/swap/hooks/useSafeBundleApprovalFlowContext' -import { useSwapFlowContext } from 'modules/swap/hooks/useSwapFlowContext' import { SwapButtonsContext } from 'modules/swap/pure/SwapButtons' import { TradeType, useTradeConfirmActions, useWrapNativeFlow } from 'modules/trade' import { useIsNativeIn } from 'modules/trade/hooks/useIsNativeInOrOut' @@ -34,18 +29,17 @@ import { QuoteDeadlineParams } from 'modules/tradeQuote' import { useApproveState } from 'common/hooks/useApproveState' import { useSafeMemo } from 'common/hooks/useSafeMemo' -import { useSafeBundleEthFlowContext } from './useSafeBundleEthFlowContext' +import { useHandleSwapOrEthFlow } from './useHandleSwapOrEthFlow' import { useDerivedSwapInfo, useSwapActionHandlers } from './useSwapState' export interface SwapButtonInput { feeWarningAccepted: boolean impactWarningAccepted: boolean - priceImpactParams: PriceImpact openNativeWrapModal(): void } export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext { - const { feeWarningAccepted, impactWarningAccepted, openNativeWrapModal, priceImpactParams } = input + const { feeWarningAccepted, impactWarningAccepted, openNativeWrapModal } = input const { account, chainId } = useWalletInfo() const { isSupportedWallet } = useWalletDetails() @@ -58,10 +52,6 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext inputError: swapInputError, } = useDerivedSwapInfo() const toggleWalletModal = useToggleWalletModal() - const swapFlowContext = useSwapFlowContext() - const ethFlowContext = useEthFlowContext() - const safeBundleApprovalFlowContext = useSafeBundleApprovalFlowContext() - const safeBundleEthFlowContext = useSafeBundleEthFlowContext() const { onCurrencySelection } = useSwapActionHandlers() const isBestQuoteLoading = useIsBestQuoteLoading() const tradeConfirmActions = useTradeConfirmActions() @@ -86,12 +76,9 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext const wrapCallback = useWrapNativeFlow() const { state: approvalState } = useApproveState(slippageAdjustedSellAmount || null) - const handleSwap = useHandleSwap(priceImpactParams) + const { callback: handleSwap, contextIsReady } = useHandleSwapOrEthFlow() - const contextExists = ethFlowContext || swapFlowContext || safeBundleApprovalFlowContext || safeBundleEthFlowContext - const recipientAddressOrName = contextExists?.orderParams.recipientAddressOrName || null - - const swapCallbackError = contextExists ? null : 'Missing dependencies' + const swapCallbackError = contextIsReady ? null : 'Missing dependencies' const gnosisSafeInfo = useGnosisSafeInfo() const isReadonlyGnosisSafeUser = gnosisSafeInfo?.isReadOnly || false @@ -106,7 +93,7 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext quoteValidTo: quote?.quoteValidTo, localQuoteTimestamp: quote?.localQuoteTimestamp, }), - [quote?.validFor, quote?.quoteValidTo, quote?.localQuoteTimestamp] + [quote?.validFor, quote?.quoteValidTo, quote?.localQuoteTimestamp], ) const swapButtonState = getSwapButtonState({ @@ -146,7 +133,6 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext toggleWalletModal, swapInputError, onCurrencySelection, - recipientAddressOrName, widgetStandaloneMode: standaloneMode, quoteDeadlineParams, }), @@ -163,10 +149,9 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext toggleWalletModal, swapInputError, onCurrencySelection, - recipientAddressOrName, standaloneMode, quoteDeadlineParams, - ] + ], ) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmButtonText.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmButtonText.tsx index 685d4c3bc7..6f249918a5 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmButtonText.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmButtonText.tsx @@ -6,9 +6,9 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Nullish } from 'types' -import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' +import { useIsSafeEthFlow } from 'modules/trade' -import { useIsSafeEthFlow } from './useIsSafeEthFlow' +import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' export function useSwapConfirmButtonText(slippageAdjustedSellAmount: Nullish>) { const isSafeApprovalBundle = useIsSafeApprovalBundle(slippageAdjustedSellAmount) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts index 77bbff00bb..afed5d89d7 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts @@ -1,53 +1,10 @@ -import { useMemo } from 'react' +import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { getWrappedToken } from '@cowprotocol/common-utils' -import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' -import { TradeType as UniTradeType } from '@uniswap/sdk-core' +import { useTradeFlowContext } from 'modules/tradeFlow' -import { useGeneratePermitHook, usePermitInfo } from 'modules/permit' -import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext' -import { SwapFlowContext } from 'modules/swap/services/types' -import { useEnoughBalanceAndAllowance } from 'modules/tokens' -import { TradeType } from 'modules/trade' +import { useSafeMemoObject } from 'common/hooks/useSafeMemo' -import { useGP2SettlementContract } from 'common/hooks/useContract' - -import { FlowType } from '../types/flowContext' - -export function useSwapFlowContext(): SwapFlowContext | null { - const contract = useGP2SettlementContract() - const baseProps = useBaseFlowContextSource() - const sellCurrency = baseProps?.trade?.inputAmount?.currency - const permitInfo = usePermitInfo(sellCurrency, TradeType.SWAP) - const generatePermitHook = useGeneratePermitHook() - - const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[baseProps?.chainId || SupportedChainId.MAINNET] - const { enoughAllowance } = useEnoughBalanceAndAllowance({ - account: baseProps?.account, - amount: baseProps?.inputAmountWithSlippage, - checkAllowanceAddress, - }) - - return useMemo(() => { - if (!baseProps?.trade) { - return null - } - - const baseContext = getFlowContext({ - baseProps, - sellToken: getWrappedToken(baseProps.trade.inputAmount.currency), - kind: baseProps.trade.tradeType === UniTradeType.EXACT_INPUT ? OrderKind.SELL : OrderKind.BUY, - }) - - if (!contract || !baseContext || baseProps.flowType !== FlowType.REGULAR) { - return null - } - - return { - ...baseContext, - contract, - permitInfo: !enoughAllowance ? permitInfo : undefined, - generatePermitHook, - } - }, [baseProps, contract, enoughAllowance, permitInfo, generatePermitHook]) +export function useSwapFlowContext() { + const [deadline] = useUserTransactionTTL() + return useTradeFlowContext(useSafeMemoObject({ deadline })) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapSlippage.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapSlippage.ts deleted file mode 100644 index 3f041d35fd..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapSlippage.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useAtomValue } from 'jotai/index' - -import { bpsToPercent } from '@cowprotocol/common-utils' -import { Percent } from '@uniswap/sdk-core' - -import { defaultSlippageAtom, smartSwapSlippageAtom, swapSlippagePercentAtom } from '../state/slippageValueAndTypeAtom' - -export function useSwapSlippage(): Percent { - return useAtomValue(swapSlippagePercentAtom) -} - -export function useDefaultSwapSlippage() { - return bpsToPercent(useAtomValue(defaultSlippageAtom)) -} - -export function useSmartSwapSlippage() { - return useAtomValue(smartSwapSlippageAtom) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index d0a98efd80..5710c36dd7 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -1,13 +1,12 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' -import { FEE_SIZE_THRESHOLD } from '@cowprotocol/common-const' import { formatSymbol, getIsNativeToken, isAddress, tryParseCurrencyAmount } from '@cowprotocol/common-utils' import { useENS } from '@cowprotocol/ens' import { useAreThereTokensWithSameSymbol, useTokenBySymbolOrAddress } from '@cowprotocol/tokens' import { Command } from '@cowprotocol/types' import { useWalletInfo } from '@cowprotocol/wallet' -import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { t } from '@lingui/macro' @@ -23,13 +22,12 @@ import { Field } from 'legacy/state/types' import { changeSwapAmountAnalytics, switchTokensAnalytics } from 'modules/analytics' import { useNavigateOnCurrencySelection } from 'modules/trade/hooks/useNavigateOnCurrencySelection' import { useTradeNavigate } from 'modules/trade/hooks/useTradeNavigate' +import { useTradeSlippage } from 'modules/tradeSlippage' import { useVolumeFee } from 'modules/volumeFee' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' import { useSafeMemo } from 'common/hooks/useSafeMemo' -import { useSwapSlippage } from './useSwapSlippage' - export const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = { '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f': true, // v2 factory '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a': true, // v2 router 01 @@ -90,14 +88,14 @@ export function useSwapActionHandlers(): SwapActions { changeSwapAmountAnalytics(field, Number(typedValue)) dispatch(typeInput({ field, typedValue })) }, - [dispatch] + [dispatch], ) const onChangeRecipient = useCallback( (recipient: string | null) => { dispatch(setRecipient({ recipient })) }, - [dispatch] + [dispatch], ) return useMemo( @@ -107,94 +105,14 @@ export function useSwapActionHandlers(): SwapActions { onUserInput, onChangeRecipient, }), - [onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient] - ) -} - -/** - * useHighFeeWarning - * @description checks whether fee vs trade inputAmount = high fee warning - * @description returns params related to high fee and a cb for checking/unchecking fee acceptance - * @param trade TradeGp param - */ -export function useHighFeeWarning(trade?: TradeGp) { - const { INPUT, OUTPUT, independentField } = useSwapState() - - const [feeWarningAccepted, setFeeWarningAccepted] = useState(false) // mod - high fee warning disable state - - // only considers inputAmount vs fee (fee is in input token) - const [isHighFee, feePercentage] = useMemo(() => { - if (!trade) return [false, undefined] - - const { outputAmountWithoutFee, inputAmountAfterFees, fee, volumeFeeAmount } = trade - const isExactInput = trade.tradeType === TradeType.EXACT_INPUT - const feeAsCurrency = isExactInput ? trade.executionPrice.quote(fee.feeAsCurrency) : fee.feeAsCurrency - - const totalFeeAmount = volumeFeeAmount ? feeAsCurrency.add(volumeFeeAmount) : feeAsCurrency - const targetAmount = isExactInput ? outputAmountWithoutFee : inputAmountAfterFees - const feePercentage = totalFeeAmount.divide(targetAmount).multiply(100).asFraction - - return [feePercentage.greaterThan(FEE_SIZE_THRESHOLD), feePercentage] - }, [trade]) - - // reset the state when users change swap params - useEffect(() => { - setFeeWarningAccepted(false) - }, [INPUT.currencyId, OUTPUT.currencyId, independentField]) - - return useSafeMemo( - () => ({ - isHighFee, - feePercentage, - // we only care/check about feeWarning being accepted if the fee is actually high.. - feeWarningAccepted: _computeFeeWarningAcceptedState({ feeWarningAccepted, isHighFee }), - setFeeWarningAccepted, - }), - [isHighFee, feePercentage, feeWarningAccepted, setFeeWarningAccepted] - ) -} - -function _computeFeeWarningAcceptedState({ - feeWarningAccepted, - isHighFee, -}: { - feeWarningAccepted: boolean - isHighFee: boolean -}) { - if (feeWarningAccepted) return true - else { - // is the fee high? that's only when we care - if (isHighFee) { - return feeWarningAccepted - } else { - return true - } - } -} - -export function useUnknownImpactWarning() { - const { INPUT, OUTPUT, independentField } = useSwapState() - - const [impactWarningAccepted, setImpactWarningAccepted] = useState(false) - - // reset the state when users change swap params - useEffect(() => { - setImpactWarningAccepted(false) - }, [INPUT.currencyId, OUTPUT.currencyId, independentField]) - - return useMemo( - () => ({ - impactWarningAccepted, - setImpactWarningAccepted, - }), - [impactWarningAccepted, setImpactWarningAccepted] + [onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient], ) } // from the current swap inputs, compute the best trade and return it. export function useDerivedSwapInfo(): DerivedSwapInfo { const { account, chainId } = useWalletInfo() - const slippage = useSwapSlippage() + const slippage = useTradeSlippage() const { independentField, @@ -220,7 +138,7 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { const isExactIn: boolean = independentField === Field.INPUT const parsedAmount = useMemo( () => tryParseCurrencyAmount(typedValue, (isExactIn ? inputCurrency : outputCurrency) ?? undefined), - [inputCurrency, isExactIn, outputCurrency, typedValue] + [inputCurrency, isExactIn, outputCurrency, typedValue], ) const currencies: { [field in Field]?: Currency | null } = useMemo( @@ -228,7 +146,7 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency, }), - [inputCurrency, outputCurrency] + [inputCurrency, outputCurrency], ) // TODO: be careful! For native tokens we use symbol instead of address @@ -243,7 +161,7 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { ? currencies.OUTPUT.symbol : currencies.OUTPUT?.address?.toLowerCase(), }), - [currencies] + [currencies], ) const { quote } = useGetQuoteAndStatus({ @@ -280,7 +198,7 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { [Field.INPUT]: inputCurrencyBalance, [Field.OUTPUT]: outputCurrencyBalance, }), - [inputCurrencyBalance, outputCurrencyBalance] + [inputCurrencyBalance, outputCurrencyBalance], ) // allowed slippage is either auto slippage, or custom user defined slippage if auto slippage disabled diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useTradeQuoteStateFromLegacy.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useTradeQuoteStateFromLegacy.ts index fa840f5c97..b9f88732f3 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useTradeQuoteStateFromLegacy.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useTradeQuoteStateFromLegacy.ts @@ -26,7 +26,8 @@ export function useTradeQuoteStateFromLegacy(): TradeQuoteState | null { quoteParams: quote || null, localQuoteTimestamp: quote?.localQuoteTimestamp || null, hasParamsChanged: isGettingNewQuote, + fetchStartTimestamp: null, }), - [quote, isLoading, isGettingNewQuote] + [quote, isLoading, isGettingNewQuote], ) } diff --git a/apps/cowswap-frontend/src/modules/swap/index.ts b/apps/cowswap-frontend/src/modules/swap/index.ts index d22922dbb8..772c226a02 100644 --- a/apps/cowswap-frontend/src/modules/swap/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/index.ts @@ -2,5 +2,5 @@ export * from './containers/SwapWidget' export * from './containers/SwapUpdaters' export * from './updaters/SwapDerivedStateUpdater' export * from './updaters/SwapAmountsFromUrlUpdater' -export * from './updaters/SmartSlippageUpdater' +export * from '../tradeSlippage/updaters/SmartSlippageUpdater' export * from './state/swapDerivedStateAtom' diff --git a/apps/cowswap-frontend/src/modules/swap/pure/ReceiveAmountInfo/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/ReceiveAmountInfo/index.tsx index 28dae3637f..61a12cd9c2 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/ReceiveAmountInfo/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/ReceiveAmountInfo/index.tsx @@ -6,8 +6,8 @@ import { Trans } from '@lingui/macro' import { BalanceAndSubsidy } from 'legacy/hooks/useCowBalanceAndSubsidy' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { getOrderTypeReceiveAmounts } from 'modules/trade' +import { useIsEoaEthFlow } from 'modules/trade' import { ReceiveAmountInfo } from 'modules/trade/types' import * as styledEl from './styled' 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 deleted file mode 100644 index ac549413d6..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { INPUT_OUTPUT_EXPLANATION, MINIMUM_ETH_FLOW_DEADLINE_SECONDS } from '@cowprotocol/common-const' -import { Command } from '@cowprotocol/types' -import { HoverTooltip, RowFixed } from '@cowprotocol/ui' - -import { Trans } from '@lingui/macro' - -import { ClickableText } from 'modules/swap/pure/Row/RowSlippageContent' -import { StyledRowBetween, TextWrapper } from 'modules/swap/pure/Row/styled' -import { RowStyleProps } from 'modules/swap/pure/Row/typings' -import { StyledInfoIcon, TransactionText } from 'modules/swap/pure/styled' - -export function getNativeOrderDeadlineTooltip(symbols: (string | undefined)[] | undefined) { - return ( - - {symbols?.[0] || 'Native currency (e.g ETH)'} orders require a minimum transaction expiration time threshold of{' '} - {MINIMUM_ETH_FLOW_DEADLINE_SECONDS / 60} minutes to ensure the best swapping experience. -
-
- Orders not matched after the threshold time are automatically refunded. -
- ) -} - -export function getNonNativeOrderDeadlineTooltip() { - return ( - - Your swap expires and will not execute if it is pending for longer than the selected duration. -
-
- {INPUT_OUTPUT_EXPLANATION} -
- ) -} - -export interface RowDeadlineProps { - toggleSettings: Command - isEoaEthFlow: boolean - symbols?: (string | undefined)[] - displayDeadline: string - styleProps?: RowStyleProps - userDeadline: number - showSettingOnClick?: boolean - slippageLabel?: React.ReactNode - slippageTooltip?: React.ReactNode -} - -// TODO: RowDeadlineContent and RowSlippageContent are very similar. Refactor and extract base component? - -export function RowDeadlineContent(props: RowDeadlineProps) { - const { showSettingOnClick, toggleSettings, displayDeadline, isEoaEthFlow, symbols, styleProps } = props - const deadlineTooltipContent = isEoaEthFlow - ? getNativeOrderDeadlineTooltip(symbols) - : getNonNativeOrderDeadlineTooltip() - - return ( - - - - {showSettingOnClick ? ( - - - - ) : ( - - )} - - - - - - - {showSettingOnClick ? ( - {displayDeadline} - ) : ( - {displayDeadline} - )} - - - ) -} - -type DeadlineTextContentsProps = { isEoaEthFlow: boolean } - -function DeadlineTextContents({ isEoaEthFlow }: DeadlineTextContentsProps) { - return ( - - Transaction expiration - {isEoaEthFlow && (modified)} - - ) -} diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/types.ts b/apps/cowswap-frontend/src/modules/swap/pure/Row/types.ts deleted file mode 100644 index f922980cf1..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RowStyleProps { - fontWeight?: number - fontSize?: number - alignContentRight?: boolean -} diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/typings.ts b/apps/cowswap-frontend/src/modules/swap/pure/Row/typings.ts deleted file mode 100644 index 8e5cd4480d..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/typings.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface RowStyleProps { - fontWeight?: number - fontSize?: number -} diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx index a6c14a7ca2..110dd701f1 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx @@ -26,7 +26,6 @@ const swapButtonsContext: SwapButtonsContext = { openSwapConfirm: () => void 0, toggleWalletModal: () => void 0, hasEnoughWrappedBalanceForSwap: true, - recipientAddressOrName: null, quoteDeadlineParams: { validFor: 0, quoteValidTo: 0, diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx index c4b93a9e3b..11c34bce1f 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx @@ -38,7 +38,6 @@ export interface SwapButtonsContext { hasEnoughWrappedBalanceForSwap: boolean swapInputError?: ReactNode onCurrencySelection: (field: Field, currency: Currency) => void - recipientAddressOrName: string | null widgetStandaloneMode?: boolean quoteDeadlineParams: QuoteDeadlineParams } diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/styled.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/styled.tsx index 5d1c870e35..71d69029e2 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/styled.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/styled.tsx @@ -23,7 +23,7 @@ export function FeesExceedFromAmountMessage() { - Costs exceed from amount + Sell amount is too small diff --git a/apps/cowswap-frontend/src/modules/swap/pure/styled.tsx b/apps/cowswap-frontend/src/modules/swap/pure/styled.tsx deleted file mode 100644 index dee470f06c..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/pure/styled.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { UI } from '@cowprotocol/ui' - -import { Info } from 'react-feather' -import styled from 'styled-components/macro' - -export const StyledInfoIcon = styled(Info)` - color: inherit; - opacity: 0.6; - line-height: 0; - vertical-align: middle; - transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; - - &:hover { - opacity: 1; - } -` - -export const TransactionText = styled.span` - display: flex; - gap: 3px; - cursor: pointer; - - > i { - font-style: normal; - } -` diff --git a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx index 5f3b3db0df..efaeef37e8 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx @@ -2,45 +2,23 @@ import React from 'react' import { genericPropsChecker } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { BundleTxApprovalBanner, BundleTxSafeWcBanner, BundleTxWrapBanner } from '@cowprotocol/ui' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' -import styled from 'styled-components/macro' - -import { HighFeeWarning, HighSuggestedSlippageWarning } from 'legacy/components/SwapWarnings' import TradeGp from 'legacy/state/swap/TradeGp' import { CompatibilityIssuesWarning } from 'modules/trade/pure/CompatibilityIssuesWarning' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { TradeUrlParams } from 'modules/trade/types/TradeRawState' - -import { ZeroApprovalWarning } from 'common/pure/ZeroApprovalWarning' +import { BundleTxWrapBanner, HighFeeWarning } from 'modules/tradeWidgetAddons' import { TwapSuggestionBanner } from './banners/TwapSuggestionBanner' export interface SwapWarningsTopProps { chainId: SupportedChainId trade: TradeGp | undefined - account: string | undefined - feeWarningAccepted: boolean - impactWarningAccepted: boolean - hideUnknownImpactWarning: boolean - showApprovalBundlingBanner: boolean - showWrapBundlingBanner: boolean - shouldZeroApprove: boolean - showSafeWcBundlingBanner: boolean showTwapSuggestionBanner: boolean - nativeCurrencySymbol: string - wrappedCurrencySymbol: string buyingFiatAmount: CurrencyAmount | null priceImpact: Percent | undefined tradeUrlParams: TradeUrlParams - isSuggestedSlippage: boolean | undefined - slippageBps: number | undefined - - setFeeWarningAccepted(cb: (state: boolean) => boolean): void - - setImpactWarningAccepted(cb: (state: boolean) => boolean): void } export interface SwapWarningsBottomProps { @@ -50,56 +28,13 @@ export interface SwapWarningsBottomProps { currencyOut: Currency | undefined } -const StyledNoImpactWarning = styled(NoImpactWarning)` - margin-bottom: 15px; -` - export const SwapWarningsTop = React.memo(function (props: SwapWarningsTopProps) { - const { - chainId, - trade, - account, - feeWarningAccepted, - impactWarningAccepted, - hideUnknownImpactWarning, - showApprovalBundlingBanner, - showWrapBundlingBanner, - showSafeWcBundlingBanner, - showTwapSuggestionBanner, - nativeCurrencySymbol, - wrappedCurrencySymbol, - setFeeWarningAccepted, - setImpactWarningAccepted, - shouldZeroApprove, - buyingFiatAmount, - priceImpact, - tradeUrlParams, - isSuggestedSlippage, - slippageBps, - } = props + const { chainId, trade, showTwapSuggestionBanner, buyingFiatAmount, priceImpact, tradeUrlParams } = props return ( <> - {shouldZeroApprove && } - setFeeWarningAccepted((state) => !state) : undefined} - /> - - {!hideUnknownImpactWarning && ( - setImpactWarningAccepted((state) => !state)} - /> - )} - {showApprovalBundlingBanner && } - {showWrapBundlingBanner && ( - - )} - {showSafeWcBundlingBanner && ( - - )} + + {showTwapSuggestionBanner && ( Promise, @@ -25,20 +27,14 @@ export async function ethFlow( tradeConfirmActions, swapFlowAnalyticsContext, context, - contract, callbacks, - appDataInfo, - dispatch, orderParams: orderParamsOriginal, - checkEthFlowOrderExists, - addInFlightOrderId, - quote, typedHooks, - } = ethFlowContext - const { - chainId, - trade: { inputAmount, outputAmount, fee }, - } = context + } = tradeContext + const { contract, appData, uploadAppData, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, quote } = + ethFlowContext + + const { chainId, inputAmount, outputAmount } = context const tradeAmounts = { inputAmount, outputAmount } const { account, recipientAddressOrName, kind } = orderParamsOriginal @@ -61,7 +57,7 @@ export async function ethFlow( // Do not proceed if fee is expired if ( isQuoteExpired({ - expirationDate: fee.expirationDate, + expirationDate: quote?.fee?.expirationDate, deadlineParams: { validFor: quote?.validFor, quoteValidTo: quote?.quoteValidTo, @@ -69,7 +65,7 @@ export async function ethFlow( }, }) ) { - reportPlaceOrderWithExpiredQuote({ ...orderParamsOriginal, fee }) + reportPlaceOrderWithExpiredQuote({ ...orderParamsOriginal, fee: quote?.fee }) throw new Error('Quote expired. Please refresh.') } @@ -105,13 +101,13 @@ export async function ethFlow( order, isSafeWallet: orderParams.isSafeWallet, }, - dispatch, + callbacks.dispatch, ) // TODO: maybe move this into addPendingOrderStep? - ethFlowContext.addTransaction({ hash: txReceipt.hash, ethFlow: { orderId: order.id, subType: 'creation' } }) + addTransaction({ hash: txReceipt.hash, ethFlow: { orderId: order.id, subType: 'creation' } }) logTradeFlow('ETH FLOW', 'STEP 6: add app data to upload queue') - callbacks.uploadAppData({ chainId: context.chainId, orderId, appData: appDataInfo }) + uploadAppData({ chainId: context.chainId, orderId, appData }) logTradeFlow('ETH FLOW', 'STEP 7: show UI of the successfully sent transaction', orderId) tradeConfirmActions.onSuccess(orderId) diff --git a/apps/cowswap-frontend/src/modules/swap/services/types.ts b/apps/cowswap-frontend/src/modules/swap/services/types.ts index e48dd370f5..cc24db27a4 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/types.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/types.ts @@ -1,78 +1,18 @@ -import { CoWSwapEthFlow, Erc20, GPv2Settlement, Weth } from '@cowprotocol/abis' -import { Command } from '@cowprotocol/types' -import { Web3Provider } from '@ethersproject/providers' -import SafeAppsSDK from '@safe-global/safe-apps-sdk' -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { CoWSwapEthFlow } from '@cowprotocol/abis' -import { AppDispatch } from 'legacy/state' import { useTransactionAdder } from 'legacy/state/enhancedTransactions/hooks' -import { AddOrderCallback } from 'legacy/state/orders/hooks' import type { QuoteInformationObject } from 'legacy/state/price/reducer' -import TradeGp from 'legacy/state/swap/TradeGp' -import { PostOrderParams } from 'legacy/utils/trade' -import { AppDataInfo, TypedAppDataHooks, UploadAppDataParams } from 'modules/appData' -import { GeneratePermitHook, IsTokenPermittableResult, useGetCachedPermit } from 'modules/permit' -import { TradeConfirmActions } from 'modules/trade' -import { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics' +import { AppDataInfo, UploadAppDataParams } from 'modules/appData' import { EthFlowOrderExistsCallback } from '../hooks/useCheckEthFlowOrderExists' -import { FlowType } from '../types/flowContext' -export interface BaseFlowContext { - context: { - chainId: number - trade: TradeGp - inputAmountWithSlippage: CurrencyAmount - outputAmountWithSlippage: CurrencyAmount - flowType: FlowType - } - flags: { - allowsOffchainSigning: boolean - } - callbacks: { - closeModals: Command - addOrderCallback: AddOrderCallback - uploadAppData: (params: UploadAppDataParams) => void - getCachedPermit: ReturnType - } - sellTokenContract: Erc20 | null - dispatch: AppDispatch - swapFlowAnalyticsContext: TradeFlowAnalyticsContext - orderParams: PostOrderParams - appDataInfo: AppDataInfo - tradeConfirmActions: TradeConfirmActions - quote: QuoteInformationObject | undefined - typedHooks?: TypedAppDataHooks -} - -export type SwapFlowContext = BaseFlowContext & { - contract: GPv2Settlement - permitInfo: IsTokenPermittableResult - generatePermitHook: GeneratePermitHook - typedHooks?: TypedAppDataHooks -} - -export type EthFlowContext = BaseFlowContext & { +export type EthFlowContext = { contract: CoWSwapEthFlow addTransaction: ReturnType checkEthFlowOrderExists: EthFlowOrderExistsCallback addInFlightOrderId: (orderId: string) => void quote: QuoteInformationObject | undefined -} - -export type BaseSafeFlowContext = BaseFlowContext & { - settlementContract: GPv2Settlement - spender: string - safeAppsSdk: SafeAppsSDK - provider: Web3Provider -} - -export type SafeBundleApprovalFlowContext = BaseSafeFlowContext & { - erc20Contract: Erc20 -} - -export type SafeBundleEthFlowContext = BaseSafeFlowContext & { - wrappedNativeContract: Weth - needsApproval: boolean + uploadAppData: (params: UploadAppDataParams) => void + appData: AppDataInfo } diff --git a/apps/cowswap-frontend/src/modules/swap/state/EthFlow/updaters/EthFlowDeadlineUpdater.tsx b/apps/cowswap-frontend/src/modules/swap/state/EthFlow/updaters/EthFlowDeadlineUpdater.tsx index 21ba267bc1..38b095c69d 100644 --- a/apps/cowswap-frontend/src/modules/swap/state/EthFlow/updaters/EthFlowDeadlineUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/swap/state/EthFlow/updaters/EthFlowDeadlineUpdater.tsx @@ -5,7 +5,7 @@ import { loadJsonFromLocalStorage, setJsonToLocalStorage } from '@cowprotocol/co import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' +import { useIsEoaEthFlow } from 'modules/trade' import { DeadlineSettings } from './types' diff --git a/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts b/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts deleted file mode 100644 index 5295aaffde..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { atom } from 'jotai' - -import { BaseFlowContextSource } from '../types/flowContext' - -export const baseFlowContextSourceAtom = atom(null) diff --git a/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts b/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts index c15bbd9a72..45b0f95097 100644 --- a/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts +++ b/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts @@ -6,13 +6,13 @@ import { OrderKind } from '@cowprotocol/cow-sdk' import { Field } from 'legacy/state/types' import { TradeType } from 'modules/trade' +import { useTradeSlippage } from 'modules/tradeSlippage' import { useTradeUsdAmounts } from 'modules/usdAmount' import { useSafeMemoObject } from 'common/hooks/useSafeMemo' import { SwapDerivedState, swapDerivedStateAtom } from './swapDerivedStateAtom' -import { useSwapSlippage } from '../hooks/useSwapSlippage' import { useDerivedSwapInfo, useSwapState } from '../hooks/useSwapState' export function useSwapDerivedState(): SwapDerivedState { @@ -23,7 +23,7 @@ export function useFillSwapDerivedState() { const { independentField, recipient, recipientAddress } = useSwapState() const { trade, currencyBalances, currencies, slippageAdjustedSellAmount, slippageAdjustedBuyAmount, parsedAmount } = useDerivedSwapInfo() - const slippage = useSwapSlippage() + const slippage = useTradeSlippage() const isSellTrade = independentField === Field.INPUT const inputCurrency = currencies.INPUT || null diff --git a/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts b/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts deleted file mode 100644 index 79b9fb126d..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Erc20, Weth } from '@cowprotocol/abis' -import type { SupportedChainId } from '@cowprotocol/cow-sdk' -import type { Command } from '@cowprotocol/types' -import type { GnosisSafeInfo } from '@cowprotocol/wallet' -import type { Web3Provider } from '@ethersproject/providers' -import type { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' - -import type { AppDispatch } from 'legacy/state' -import type { AddOrderCallback } from 'legacy/state/orders/hooks' -import type { QuoteInformationObject } from 'legacy/state/price/reducer' -import type TradeGp from 'legacy/state/swap/TradeGp' - -import type { useGetCachedPermit } from 'modules/permit' -import type { TradeConfirmActions } from 'modules/trade' - -import type { AppDataInfo, TypedAppDataHooks, UploadAppDataParams } from '../../appData' - -export enum FlowType { - REGULAR = 'REGULAR', - EOA_ETH_FLOW = 'EOA_ETH_FLOW', - SAFE_BUNDLE_APPROVAL = 'SAFE_BUNDLE_APPROVAL', - SAFE_BUNDLE_ETH = 'SAFE_BUNDLE_ETH', -} - -export interface BaseFlowContextSource { - chainId: SupportedChainId - account: string | undefined - sellTokenContract: Erc20 | null - provider: Web3Provider | undefined - trade: TradeGp | undefined - appData: AppDataInfo | null - wethContract: Weth | null - inputAmountWithSlippage: CurrencyAmount | undefined - outputAmountWithSlippage: CurrencyAmount | undefined - gnosisSafeInfo: GnosisSafeInfo | undefined - recipient: string | null - recipientAddressOrName: string | null - deadline: number - ensRecipientAddress: string | null - allowsOffchainSigning: boolean - flowType: FlowType - closeModals: Command - uploadAppData: (update: UploadAppDataParams) => void - addOrderCallback: AddOrderCallback - dispatch: AppDispatch - allowedSlippage: Percent - tradeConfirmActions: TradeConfirmActions - getCachedPermit: ReturnType - quote: QuoteInformationObject | undefined - typedHooks: TypedAppDataHooks | undefined -} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx b/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx deleted file mode 100644 index 12857e8c11..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useSetAtom } from 'jotai' -import { useEffect } from 'react' - -import { getAddress } from '@cowprotocol/common-utils' -import { useENSAddress } from '@cowprotocol/ens' -import { useGnosisSafeInfo, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' -import { useWalletProvider } from '@cowprotocol/wallet-provider' - -import { useDispatch } from 'react-redux' - -import { AppDispatch } from 'legacy/state' -import { useCloseModals } from 'legacy/state/application/hooks' -import { useAddPendingOrder } from 'legacy/state/orders/hooks' -import { useGetQuoteAndStatus } from 'legacy/state/price/hooks' -import { useUserTransactionTTL } from 'legacy/state/user/hooks' - -import { useAppData, useAppDataHooks, useUploadAppData } from 'modules/appData' -import { useGetCachedPermit } from 'modules/permit' -import { useTradeConfirmActions } from 'modules/trade' - -import { useTokenContract, useWETHContract } from 'common/hooks/useContract' -import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' -import { useSafeMemo } from 'common/hooks/useSafeMemo' - -import { useSwapAmountsWithSlippage } from '../hooks/useFlowContext' -import { useIsEoaEthFlow } from '../hooks/useIsEoaEthFlow' -import { useIsSafeEthFlow } from '../hooks/useIsSafeEthFlow' -import { useSwapSlippage } from '../hooks/useSwapSlippage' -import { useDerivedSwapInfo, useSwapState } from '../hooks/useSwapState' -import { baseFlowContextSourceAtom } from '../state/baseFlowContextSourceAtom' -import { FlowType } from '../types/flowContext' - -export function BaseFlowContextUpdater() { - const setBaseFlowContextSource = useSetAtom(baseFlowContextSourceAtom) - const provider = useWalletProvider() - const { account, chainId } = useWalletInfo() - const { allowsOffchainSigning } = useWalletDetails() - const gnosisSafeInfo = useGnosisSafeInfo() - const { recipient } = useSwapState() - const slippage = useSwapSlippage() - const { trade, currenciesIds } = useDerivedSwapInfo() - const { quote } = useGetQuoteAndStatus({ - token: currenciesIds.INPUT, - chainId, - }) - - const appData = useAppData() - const typedHooks = useAppDataHooks() - const closeModals = useCloseModals() - const uploadAppData = useUploadAppData() - const addOrderCallback = useAddPendingOrder() - const dispatch = useDispatch() - const tradeConfirmActions = useTradeConfirmActions() - - const { address: ensRecipientAddress } = useENSAddress(recipient) - const recipientAddressOrName = recipient || ensRecipientAddress - const [deadline] = useUserTransactionTTL() - const wethContract = useWETHContract() - const isEoaEthFlow = useIsEoaEthFlow() - const isSafeEthFlow = useIsSafeEthFlow() - const getCachedPermit = useGetCachedPermit() - - const [inputAmountWithSlippage, outputAmountWithSlippage] = useSwapAmountsWithSlippage() - const sellTokenContract = useTokenContract(getAddress(inputAmountWithSlippage?.currency) || undefined, true) - - const isSafeBundle = useIsSafeApprovalBundle(inputAmountWithSlippage) - const flowType = getFlowType(isSafeBundle, isEoaEthFlow, isSafeEthFlow) - - const source = useSafeMemo( - () => ({ - chainId, - account, - sellTokenContract, - provider, - trade, - appData, - wethContract, - inputAmountWithSlippage, - outputAmountWithSlippage, - gnosisSafeInfo, - recipient, - recipientAddressOrName, - deadline, - ensRecipientAddress, - allowsOffchainSigning, - uploadAppData, - flowType, - closeModals, - addOrderCallback, - dispatch, - allowedSlippage: slippage, - tradeConfirmActions, - getCachedPermit, - quote, - typedHooks, - }), - [ - chainId, - account, - sellTokenContract, - provider, - trade, - appData, - wethContract, - inputAmountWithSlippage, - outputAmountWithSlippage, - gnosisSafeInfo, - recipient, - recipientAddressOrName, - deadline, - ensRecipientAddress, - allowsOffchainSigning, - uploadAppData, - flowType, - closeModals, - addOrderCallback, - dispatch, - slippage, - tradeConfirmActions, - getCachedPermit, - quote, - typedHooks, - ], - ) - - useEffect(() => { - setBaseFlowContextSource(source) - }, [source, setBaseFlowContextSource]) - - return null -} - -function getFlowType(isSafeBundle: boolean, isEoaEthFlow: boolean, isSafeEthFlow: boolean): FlowType { - if (isSafeEthFlow) { - // Takes precedence over bundle approval - return FlowType.SAFE_BUNDLE_ETH - } - if (isSafeBundle) { - // Takes precedence over eth flow - return FlowType.SAFE_BUNDLE_APPROVAL - } - if (isEoaEthFlow) { - // Takes precedence over regular flow - return FlowType.EOA_ETH_FLOW - } - return FlowType.REGULAR -} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/NoImpactWarning/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/NoImpactWarning/index.tsx new file mode 100644 index 0000000000..cd69f633ba --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/containers/NoImpactWarning/index.tsx @@ -0,0 +1,73 @@ +import { atom, useAtom, useAtomValue } from 'jotai' +import { useEffect } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import { useTradePriceImpact } from 'modules/trade' +import { TradeWarning, TradeWarningType } from 'modules/trade/pure/TradeWarning' +import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFormValidation' +import { useTradeQuote } from 'modules/tradeQuote' + +const noImpactWarningAcceptedAtom = atom(false) + +const NoImpactWarningMessage = ( +
+ + We are unable to calculate the price impact for this order. +
+
+ You may still move forward but{' '} + please review carefully that the receive amounts are what you expect. +
+
+) + +export function useIsNoImpactWarningAccepted() { + return useAtomValue(noImpactWarningAcceptedAtom) +} + +export interface NoImpactWarningProps { + withoutAccepting?: boolean + className?: string +} + +export function NoImpactWarning(props: NoImpactWarningProps) { + const { withoutAccepting, className } = props + + const [isAccepted, setIsAccepted] = useAtom(noImpactWarningAcceptedAtom) + + const { account } = useWalletInfo() + const priceImpactParams = useTradePriceImpact() + const primaryFormValidation = useGetTradeFormValidation() + const tradeQuote = useTradeQuote() + + const canTrade = + (primaryFormValidation === null || primaryFormValidation === TradeFormValidation.ApproveAndSwap) && + !tradeQuote.error + + const showPriceImpactWarning = canTrade && !!account && !priceImpactParams.loading && !priceImpactParams.priceImpact + + const acceptCallback = () => setIsAccepted((state) => !state) + + useEffect(() => { + setIsAccepted(!showPriceImpactWarning) + }, [showPriceImpactWarning, setIsAccepted]) + + if (!showPriceImpactWarning) return null + + return ( + + Price impact unknown - trade carefully + + } + /> + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/index.tsx index b049e09b4a..0560ff91f0 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/index.tsx @@ -1,12 +1,12 @@ -import { Dispatch, ReactNode, SetStateAction, useMemo } from 'react' +import { ReactNode, useMemo, useState } from 'react' import { FractionUtils } from '@cowprotocol/common-utils' import { PercentDisplay } from '@cowprotocol/ui' -import { CowSwapWidgetAppParams } from '@cowprotocol/widget-lib' import { Percent, Price } from '@uniswap/sdk-core' import { Nullish } from 'types' +import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useUsdAmount } from 'modules/usdAmount' import { RateInfoParams } from 'common/pure/RateInfo' @@ -24,9 +24,7 @@ import { TradeFeesAndCosts } from '../TradeFeesAndCosts' type Props = { receiveAmountInfo: ReceiveAmountInfo rateInfoParams: RateInfoParams - isInvertedState: [boolean, Dispatch>] slippage: Percent - widgetParams: Partial labelsAndTooltips?: LabelsAndTooltips children?: ReactNode recipient?: Nullish @@ -53,11 +51,9 @@ type LabelsAndTooltips = { export function TradeBasicConfirmDetails(props: Props) { const { rateInfoParams, - isInvertedState, slippage, labelsAndTooltips, receiveAmountInfo, - widgetParams, hideLimitPrice, hideUsdValues, withTimelineDot = true, @@ -66,6 +62,8 @@ export function TradeBasicConfirmDetails(props: Props) { recipient, account, } = props + const isInvertedState = useState(false) + const widgetParams = useInjectedWidgetParams() const { amountAfterFees, amountAfterSlippage } = getOrderTypeReceiveAmounts(receiveAmountInfo) const { networkCostsSuffix, networkCostsTooltipSuffix } = labelsAndTooltips || {} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.cosmos.tsx index f4e50e9ed7..428a89e636 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.cosmos.tsx @@ -70,7 +70,7 @@ function Custom({ stateValue }: { stateValue: string }) { return ( - Some content + {() => Some content} ) diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWarnings/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWarnings/index.tsx new file mode 100644 index 0000000000..a3d98716b4 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWarnings/index.tsx @@ -0,0 +1,65 @@ +import React from 'react' + +import { CowSwapSafeAppLink, InlineBanner } from '@cowprotocol/ui' +import { useIsSafeViaWc } from '@cowprotocol/wallet' + +import { useInjectedWidgetParams } from 'modules/injectedWidget' +import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFormValidation' +import { HighSuggestedSlippageWarning } from 'modules/tradeSlippage' +import { useShouldZeroApprove } from 'modules/zeroApproval' + +import { useReceiveAmountInfo } from '../../hooks/useReceiveAmountInfo' +import { ZeroApprovalWarning } from '../../pure/ZeroApprovalWarning' +import { NoImpactWarning } from '../NoImpactWarning' + +interface TradeWarningsProps { + isTradePriceUpdating: boolean + enableSmartSlippage?: boolean +} + +export function TradeWarnings({ isTradePriceUpdating, enableSmartSlippage }: TradeWarningsProps) { + const { banners: widgetBanners } = useInjectedWidgetParams() + const primaryFormValidation = useGetTradeFormValidation() + const receiveAmountInfo = useReceiveAmountInfo() + const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount + const shouldZeroApprove = useShouldZeroApprove(inputAmountWithSlippage) + const isSafeViaWc = useIsSafeViaWc() + + const showBundleTxApprovalBanner = primaryFormValidation === TradeFormValidation.ApproveAndSwap + const showSafeWcBundlingBanner = + isSafeViaWc && primaryFormValidation === TradeFormValidation.ApproveRequired && !widgetBanners?.hideSafeWebAppBanner + + return ( + <> + {shouldZeroApprove && } + + {showBundleTxApprovalBanner && } + {showSafeWcBundlingBanner && } + {enableSmartSlippage && } + + ) +} + +function BundleTxApprovalBanner() { + return ( + + Token approval bundling +

+ For your convenience, token approval and order placement will be bundled into a single transaction, streamlining + your experience! +

+
+ ) +} + +function BundleTxSafeWcBanner() { + return ( + + Use Safe web app +

+ Use the Safe web app for streamlined trading: token approval and orders bundled in one go! Only available in the{' '} + +

+
+ ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index 100517aa7a..7af3c59f62 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import ICON_ORDERS from '@cowprotocol/assets/svg/orders.svg' import ICON_TOKENS from '@cowprotocol/assets/svg/tokens.svg' @@ -35,6 +35,7 @@ import { useTradeStateFromUrl } from '../../hooks/setupTradeState/useTradeStateF import { useIsWrapOrUnwrap } from '../../hooks/useIsWrapOrUnwrap' import { useTradeTypeInfo } from '../../hooks/useTradeTypeInfo' import { TradeType } from '../../types' +import { TradeWarnings } from '../TradeWarnings' import { TradeWidgetLinks } from '../TradeWidgetLinks' import { WrapFlowActionButton } from '../WrapFlowActionButton' @@ -59,7 +60,16 @@ export function TradeWidgetForm(props: TradeWidgetProps) { const { settingsWidget, lockScreen, topContent, middleContent, bottomContent, outerContent } = slots const { onCurrencySelection, onUserInput, onSwitchTokens, onChangeRecipient } = actions - const { compactView, showRecipient, isTradePriceUpdating, isEoaEthFlow = false, priceImpact, recipient } = params + const { + compactView, + showRecipient, + isTradePriceUpdating, + isEoaEthFlow = false, + priceImpact, + recipient, + hideTradeWarnings, + enableSmartSlippage, + } = params const inputCurrencyInfo = useMemo( () => (isWrapOrUnwrap ? { ...props.inputCurrencyInfo, receiveAmountInfo: null } : props.inputCurrencyInfo), @@ -200,7 +210,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { isCollapsed={compactView} hasSeparatorLine={!compactView} onSwitchTokens={isChainIdUnsupported ? () => void 0 : throttledOnSwitchTokens} - isLoading={isTradePriceUpdating} + isLoading={Boolean(inputCurrencyInfo.currency && outputCurrencyInfo.currency && isTradePriceUpdating)} disabled={isAlternativeOrderModalVisible} /> @@ -221,7 +231,18 @@ export function TradeWidgetForm(props: TradeWidgetProps) { {withRecipient && } - {isWrapOrUnwrap ? : bottomContent} + {isWrapOrUnwrap ? ( + + ) : ( + bottomContent?.( + hideTradeWarnings ? null : ( + + ), + ) + )} )} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx index f7623ca734..ae2243872b 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx @@ -5,6 +5,7 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { TradeFormValidationUpdater } from 'modules/tradeFormValidation' import { TradeQuoteState, TradeQuoteUpdater, useUpdateTradeQuote } from 'modules/tradeQuote' +import { SmartSlippageUpdater } from 'modules/tradeSlippage' import { usePriorityTokenAddresses } from '../../hooks/usePriorityTokenAddresses' import { useResetRecipient } from '../../hooks/useResetRecipient' @@ -16,6 +17,7 @@ import { RecipientAddressUpdater } from '../../updaters/RecipientAddressUpdater' interface TradeWidgetUpdatersProps { disableQuotePolling: boolean disableNativeSelling: boolean + enableSmartSlippage?: boolean children: ReactNode tradeQuoteStateOverride?: TradeQuoteState | null onChangeRecipient: (recipient: string | null) => void @@ -25,6 +27,7 @@ export function TradeWidgetUpdaters({ disableQuotePolling, disableNativeSelling, tradeQuoteStateOverride, + enableSmartSlippage, onChangeRecipient, children, }: TradeWidgetUpdatersProps) { @@ -49,6 +52,7 @@ export function TradeWidgetUpdaters({ + {enableSmartSlippage && } {disableNativeSelling && } {children} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx index fc9935a4c9..764652582f 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx @@ -8,7 +8,12 @@ export const TradeWidgetContainer = styledEl.Container export function TradeWidget(props: TradeWidgetProps) { const { id, slots, params, confirmModal, genericModal } = props - const { disableQuotePolling = false, disableNativeSelling = false, tradeQuoteStateOverride } = params + const { + disableQuotePolling = false, + disableNativeSelling = false, + tradeQuoteStateOverride, + enableSmartSlippage, + } = params const modals = TradeWidgetModals(confirmModal, genericModal) return ( @@ -18,6 +23,7 @@ export function TradeWidget(props: TradeWidgetProps) { disableQuotePolling={disableQuotePolling} disableNativeSelling={disableNativeSelling} tradeQuoteStateOverride={tradeQuoteStateOverride} + enableSmartSlippage={enableSmartSlippage} onChangeRecipient={props.actions.onChangeRecipient} > {slots.updaters} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts index 396db9923c..d0e0e8e3d8 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts @@ -26,6 +26,8 @@ interface TradeWidgetParams { disableQuotePolling?: boolean disableNativeSelling?: boolean disablePriceImpact?: boolean + hideTradeWarnings?: boolean + enableSmartSlippage?: boolean } export interface TradeWidgetSlots { @@ -33,7 +35,7 @@ export interface TradeWidgetSlots { lockScreen?: ReactNode topContent?: ReactNode middleContent?: ReactNode - bottomContent?: ReactNode + bottomContent?(warnings: ReactNode | null): ReactNode outerContent?: ReactNode updaters?: ReactNode } diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx index 65bf8dda31..c0874b0fc3 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, useCallback } from 'react' +import { useCallback, useMemo, useState } from 'react' import { Command } from '@cowprotocol/types' import { Badge } from '@cowprotocol/ui' @@ -7,7 +7,7 @@ import type { TradeType } from '@cowprotocol/widget-lib' import { Trans } from '@lingui/macro' import IMAGE_CARET from 'assets/icon/caret.svg' import SVG from 'react-inlinesvg' -import { matchPath, useLocation } from 'react-router-dom' +import { useLocation } from 'react-router-dom' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { ModalHeader } from 'modules/tokensList/pure/ModalHeader' @@ -18,7 +18,9 @@ import { useMenuItems } from 'common/hooks/useMenuItems' import * as styledEl from './styled' import { useTradeRouteContext } from '../../hooks/useTradeRouteContext' -import { parameterizeTradeRoute } from '../../utils/parameterizeTradeRoute' +import { useGetTradeStateByRoute } from '../../hooks/useTradeState' +import { getDefaultTradeRawState, TradeUrlParams } from '../../types/TradeRawState' +import { addChainIdToRoute, parameterizeTradeRoute } from '../../utils/parameterizeTradeRoute' interface MenuItemConfig { route: RoutesValues @@ -30,6 +32,7 @@ const TRADE_TYPE_TO_ROUTE: Record = { swap: Routes.SWAP, limit: Routes.LIMIT_ORDER, advanced: Routes.ADVANCED_ORDERS, + yield: Routes.YIELD, } interface TradeWidgetLinksProps { @@ -42,6 +45,7 @@ export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) const [isDropdownVisible, setDropdownVisible] = useState(false) const { enabledTradeTypes } = useInjectedWidgetParams() const menuItems = useMenuItems() + const getTradeStateByType = useGetTradeStateByRoute() const handleMenuItemClick = useCallback((_item?: MenuItemConfig): void => { setDropdownVisible(false) @@ -57,8 +61,28 @@ export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) const menuItemsElements: JSX.Element[] = useMemo(() => { return enabledItems.map((item) => { - const routePath = parameterizeTradeRoute(tradeContext, item.route, true) - const isActive = !!matchPath(location.pathname, routePath.split('?')[0]) + const isItemYield = item.route === Routes.YIELD + const chainId = tradeContext.chainId + + const isCurrentPathYield = location.pathname.startsWith(addChainIdToRoute(Routes.YIELD, chainId)) + const itemTradeState = getTradeStateByType(item.route) + + const routePath = isItemYield + ? addChainIdToRoute(item.route, chainId) + : parameterizeTradeRoute( + isCurrentPathYield + ? ({ + chainId, + inputCurrencyId: + itemTradeState.inputCurrencyId || (chainId && getDefaultTradeRawState(+chainId).inputCurrencyId), + outputCurrencyId: itemTradeState.outputCurrencyId, + } as TradeUrlParams) + : tradeContext, + item.route, + !isCurrentPathYield, + ) + + const isActive = location.pathname.startsWith(routePath.split('?')[0]) return ( ) }) - }, [isDropdown, isDropdownVisible, enabledItems, tradeContext, location.pathname, handleMenuItemClick]) + }, [ + isDropdown, + isDropdownVisible, + enabledItems, + tradeContext, + location.pathname, + handleMenuItemClick, + getTradeStateByType, + ]) const singleMenuItem = menuItemsElements.length === 1 diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeStateFromUrl.ts b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeStateFromUrl.ts index f6a38792dd..5acb220f60 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeStateFromUrl.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeStateFromUrl.ts @@ -1,11 +1,12 @@ import { useSetAtom } from 'jotai' -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { useLocation, useParams } from 'react-router-dom' import { tradeStateFromUrlAtom } from 'modules/trade/state/tradeStateFromUrlAtom' import { TradeRawState } from '../../types/TradeRawState' +import { useTradeState } from '../useTradeState' /** * Updater to fetch trade state from URL params and query, and store it on jotai state @@ -18,6 +19,9 @@ export function useSetupTradeStateFromUrl(): null { const location = useLocation() const stringifiedParams = JSON.stringify(params) const setState = useSetAtom(tradeStateFromUrlAtom) + const { state } = useTradeState() + const tradeStateRef = useRef(state) + tradeStateRef.current = state useEffect(() => { const searchParams = new URLSearchParams(location.search) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsEoaEthFlow.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useIsEoaEthFlow.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsEoaEthFlow.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useIsEoaEthFlow.ts diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSafeEthFlow.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useIsSafeEthFlow.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsSafeEthFlow.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useIsSafeEthFlow.ts diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSwapEth.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useIsSwapEth.ts similarity index 53% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsSwapEth.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useIsSwapEth.ts index 30b3a94d08..b73d67afca 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSwapEth.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useIsSwapEth.ts @@ -1,5 +1,5 @@ -import { useIsNativeIn } from 'modules/trade/hooks/useIsNativeInOrOut' -import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap' +import { useIsNativeIn } from './useIsNativeInOrOut' +import { useIsWrapOrUnwrap } from './useIsWrapOrUnwrap' export function useIsSwapEth(): boolean { const isNativeIn = useIsNativeIn() diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useNavigateToNewOrderCallback.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateToNewOrderCallback.ts similarity index 83% rename from apps/cowswap-frontend/src/modules/swap/hooks/useNavigateToNewOrderCallback.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useNavigateToNewOrderCallback.ts index 43ee8fe36c..3e35a29f40 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useNavigateToNewOrderCallback.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateToNewOrderCallback.ts @@ -9,10 +9,10 @@ import { Order } from 'legacy/state/orders/actions' import { Routes } from 'common/constants/routes' import { useNavigate } from 'common/hooks/useNavigate' -import { parameterizeTradeRoute } from '../../trade' -import { TradeUrlParams } from '../../trade/types/TradeRawState' +import { TradeUrlParams } from '../types/TradeRawState' +import { parameterizeTradeRoute } from '../utils/parameterizeTradeRoute' -export type NavigateToNewOrderCallback = (chainId: SupportedChainId, order?: Order, callback?: Command) => () => void +type NavigateToNewOrderCallback = (chainId: SupportedChainId, order?: Order, callback?: Command) => () => void export function useNavigateToNewOrderCallback(): NavigateToNewOrderCallback { const navigate = useNavigate() @@ -41,6 +41,6 @@ export function useNavigateToNewOrderCallback(): NavigateToNewOrderCallback { callback?.() } }, - [navigate] + [navigate], ) } diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts index 0d3b539695..38d74eae3b 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts @@ -36,6 +36,7 @@ const TradeTypeToUiOrderType: Record = { [TradeType.SWAP]: UiOrderType.SWAP, [TradeType.LIMIT_ORDER]: UiOrderType.LIMIT, [TradeType.ADVANCED_ORDERS]: UiOrderType.TWAP, + [TradeType.YIELD]: UiOrderType.YIELD, } function getTradeParamsEventPayload(tradeType: TradeType, state: TradeDerivedState): OnTradeParamsPayload { diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useOrderSubmittedContent.tsx b/apps/cowswap-frontend/src/modules/trade/hooks/useOrderSubmittedContent.tsx new file mode 100644 index 0000000000..bf7c5b6d15 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useOrderSubmittedContent.tsx @@ -0,0 +1,35 @@ +import { useCallback } from 'react' + +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Command } from '@cowprotocol/types' + +import { useOrder } from 'legacy/state/orders/hooks' + +import { useOrderProgressBarV2Props } from 'common/hooks/orderProgressBarV2' +import { TransactionSubmittedContent } from 'common/pure/TransactionSubmittedContent' + +import { useNavigateToNewOrderCallback } from './useNavigateToNewOrderCallback' +import { useTradeConfirmState } from './useTradeConfirmState' + +export function useOrderSubmittedContent(chainId: SupportedChainId) { + const { transactionHash } = useTradeConfirmState() + const order = useOrder({ chainId, id: transactionHash || undefined }) + + const orderProgressBarV2Props = useOrderProgressBarV2Props(chainId, order) + + const navigateToNewOrderCallback = useNavigateToNewOrderCallback() + + return useCallback( + (onDismiss: Command) => ( + + ), + [chainId, transactionHash, orderProgressBarV2Props, navigateToNewOrderCallback], + ) +} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useShouldPayGas.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useShouldPayGas.ts similarity index 82% rename from apps/cowswap-frontend/src/modules/swap/hooks/useShouldPayGas.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useShouldPayGas.ts index bb0f2fc0b6..eb9ca97852 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useShouldPayGas.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useShouldPayGas.ts @@ -1,6 +1,6 @@ import { useWalletDetails } from '@cowprotocol/wallet' -import { useIsEoaEthFlow } from './useIsEoaEthFlow' +import { useIsEoaEthFlow } from 'modules/trade' export function useShouldPayGas() { const { allowsOffchainSigning } = useWalletDetails() diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeState.ts index 213cd9002f..60631701d7 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeState.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { useAdvancedOrdersRawState, @@ -7,6 +7,9 @@ import { import { useLimitOrdersRawState, useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' import { useSwapRawState, useUpdateSwapRawState } from 'modules/swap/hooks/useSwapRawState' import { ExtendedTradeRawState, TradeRawState } from 'modules/trade/types/TradeRawState' +import { useUpdateYieldRawState, useYieldRawState } from 'modules/yield' + +import { Routes, RoutesValues } from 'common/constants/routes' import { useTradeTypeInfoFromUrl } from './useTradeTypeInfoFromUrl' @@ -29,6 +32,9 @@ export function useTradeState(): { const swapTradeState = useSwapRawState() const updateSwapState = useUpdateSwapRawState() + const yieldRawState = useYieldRawState() + const updateYieldRawState = useUpdateYieldRawState() + return useMemo(() => { if (!tradeTypeInfo) return EMPTY_TRADE_STATE @@ -46,6 +52,13 @@ export function useTradeState(): { } } + if (tradeTypeInfo.tradeType === TradeType.YIELD) { + return { + state: yieldRawState, + updateState: updateYieldRawState, + } + } + return { state: limitOrdersState, updateState: updateLimitOrdersState, @@ -60,7 +73,27 @@ export function useTradeState(): { JSON.stringify(advancedOrdersState), // eslint-disable-next-line react-hooks/exhaustive-deps JSON.stringify(swapTradeState), + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(yieldRawState), updateSwapState, updateLimitOrdersState, + updateYieldRawState, ]) } + +export function useGetTradeStateByRoute() { + const limitOrdersState = useLimitOrdersRawState() + const advancedOrdersState = useAdvancedOrdersRawState() + const swapTradeState = useSwapRawState() + const yieldRawState = useYieldRawState() + + return useCallback( + (route: RoutesValues) => { + if (route === Routes.SWAP || route === Routes.HOOKS) return swapTradeState + if (route === Routes.ABOUT) return advancedOrdersState + if (route === Routes.YIELD) return yieldRawState + return limitOrdersState + }, + [swapTradeState, advancedOrdersState, yieldRawState, limitOrdersState], + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeTypeInfoFromUrl.tsx b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeTypeInfoFromUrl.tsx index c5ee725884..0774836eae 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeTypeInfoFromUrl.tsx +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeTypeInfoFromUrl.tsx @@ -12,15 +12,17 @@ export function useTradeTypeInfoFromUrl(): TradeTypeInfo | null { const hooksMatch = !!useMatchTradeRoute('swap/hooks') const limitOrderMatch = !!useMatchTradeRoute('limit') const advancedOrdersMatch = !!useMatchTradeRoute('advanced') + const yieldMatch = !!useMatchTradeRoute('yield') return useMemo(() => { if (hooksMatch) return { tradeType: TradeType.SWAP, route: Routes.HOOKS } if (swapMatch) return { tradeType: TradeType.SWAP, route: Routes.SWAP } if (limitOrderMatch) return { tradeType: TradeType.LIMIT_ORDER, route: Routes.LIMIT_ORDER } if (advancedOrdersMatch) return { tradeType: TradeType.ADVANCED_ORDERS, route: Routes.ADVANCED_ORDERS } + if (yieldMatch) return { tradeType: TradeType.YIELD, route: Routes.YIELD } return null - }, [swapMatch, limitOrderMatch, advancedOrdersMatch]) + }, [swapMatch, hooksMatch, limitOrderMatch, advancedOrdersMatch, yieldMatch]) } function useMatchTradeRoute(route: string): PathMatch<'chainId'> | null { diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useUnknownImpactWarning.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useUnknownImpactWarning.ts new file mode 100644 index 0000000000..e7665060e8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useUnknownImpactWarning.ts @@ -0,0 +1,29 @@ +import { useMemo, useState } from 'react' + +import { useSafeEffect } from 'common/hooks/useSafeMemo' + +import { useReceiveAmountInfo } from './useReceiveAmountInfo' + +export function useUnknownImpactWarning() { + const receiveAmountInfo = useReceiveAmountInfo() + + const state = useState(false) + const [impactWarningAccepted, setImpactWarningAccepted] = state + + // reset the state when users change swap params + useSafeEffect(() => { + setImpactWarningAccepted(false) + }, [ + receiveAmountInfo?.beforeNetworkCosts.sellAmount.currency, + receiveAmountInfo?.beforeNetworkCosts.buyAmount.currency, + receiveAmountInfo?.isSell, + ]) + + return useMemo( + () => ({ + impactWarningAccepted, + setImpactWarningAccepted, + }), + [impactWarningAccepted, setImpactWarningAccepted], + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/index.ts b/apps/cowswap-frontend/src/modules/trade/index.ts index c08f839ed6..10c978158e 100644 --- a/apps/cowswap-frontend/src/modules/trade/index.ts +++ b/apps/cowswap-frontend/src/modules/trade/index.ts @@ -3,6 +3,7 @@ export * from './containers/TradeConfirmModal' export * from './containers/TradeWidgetLinks' export * from './containers/TradeFeesAndCosts' export * from './containers/TradeTotalCostsDetails' +export * from './containers/TradeBasicConfirmDetails' export * from './pure/TradeConfirmation' export * from './hooks/useTradeConfirmActions' export * from './hooks/useTradeTypeInfo' @@ -25,13 +26,26 @@ export * from './hooks/useIsWrapOrUnwrap' export * from './hooks/useIsHooksTradeType' export * from './hooks/useHasTradeEnoughAllowance' export * from './hooks/useIsSellNative' +export * from './hooks/useBuildTradeDerivedState' +export * from './hooks/useOnCurrencySelection' +export * from './hooks/useDerivedTradeState' +export * from './hooks/useNavigateToNewOrderCallback' +export * from './hooks/useOrderSubmittedContent' +export * from './hooks/useIsEoaEthFlow' +export * from './hooks/useShouldPayGas' +export * from './hooks/useWrappedToken' +export * from './hooks/useUnknownImpactWarning' +export * from './hooks/useIsSwapEth' +export * from './hooks/useIsSafeEthFlow' export * from './containers/TradeWidget/types' +export { useIsNoImpactWarningAccepted } from './containers/NoImpactWarning/index' export * from './utils/getReceiveAmountInfo' export * from './utils/parameterizeTradeRoute' export * from './state/receiveAmountInfoAtom' export * from './state/tradeTypeAtom' export * from './state/derivedTradeStateAtom' export * from './state/isWrapOrUnwrapAtom' +export * from './state/isEoaEthFlowAtom' export * from './pure/RecipientRow' export * from './pure/ReceiveAmountTitle' export * from './pure/PartnerFeeRow' diff --git a/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/index.tsx index 75612ae86f..65f4bbc52c 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/index.tsx @@ -5,10 +5,10 @@ import { InfoTooltip } from '@cowprotocol/ui' import { CornerDownRight } from 'react-feather' -import { TimelineDot } from 'modules/trade/pure/Row/styled' - import { Content, Row, Wrapper, Label } from './styled' +import { TimelineDot } from '../Row/styled' + export type ConfirmDetailsItemProps = { children: ReactNode label?: ReactNode diff --git a/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/styled.ts b/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/styled.ts index a442a84135..7416efef16 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/styled.ts +++ b/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/styled.ts @@ -2,7 +2,7 @@ import { Media, UI } from '@cowprotocol/ui' import styled, { css } from 'styled-components/macro' -import { StyledRowBetween } from 'modules/swap/pure/Row/styled' +import { StyledRowBetween } from 'modules/tradeWidgetAddons/pure/Row/styled' export const Wrapper = styled.div<{ alwaysRow: boolean }>` display: flex; @@ -83,7 +83,9 @@ export const Label = styled.span<{ labelOpacity?: boolean }>` gap: 5px; text-align: left; opacity: ${({ labelOpacity }) => (labelOpacity ? 0.7 : 1)}; - transition: color var(${UI.ANIMATION_DURATION}) ease-in-out, opacity var(${UI.ANIMATION_DURATION}) ease-in-out; + transition: + color var(${UI.ANIMATION_DURATION}) ease-in-out, + opacity var(${UI.ANIMATION_DURATION}) ease-in-out; color: inherit; &:hover { diff --git a/apps/cowswap-frontend/src/modules/trade/pure/NoImpactWarning/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/NoImpactWarning/index.tsx deleted file mode 100644 index 4b339bb851..0000000000 --- a/apps/cowswap-frontend/src/modules/trade/pure/NoImpactWarning/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { TradeWarning, TradeWarningType } from 'modules/trade/pure/TradeWarning' - -const NoImpactWarningMessage = ( -
- - We are unable to calculate the price impact for this order. -
-
- You may still move forward but{' '} - please review carefully that the receive amounts are what you expect. -
-
-) - -export interface NoImpactWarningProps { - isAccepted: boolean - withoutAccepting?: boolean - className?: string - acceptCallback?(): void -} - -export function NoImpactWarning(props: NoImpactWarningProps) { - const { acceptCallback, isAccepted, withoutAccepting, className } = props - - return ( - - Price impact unknown - trade carefully - - } - /> - ) -} diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.cosmos.tsx index 39ffc51975..18263525af 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.cosmos.tsx @@ -17,7 +17,7 @@ const Fixtures = { refreshInterval={10_000} recipient={null} > - Trade confirmation + {() => Trade confirmation} ), } diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx index 31cd765195..d8b6e7ab8b 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx @@ -25,6 +25,7 @@ import { QuoteCountdown } from './CountDown' import { useIsPriceChanged } from './hooks/useIsPriceChanged' import * as styledEl from './styled' +import { NoImpactWarning } from '../../containers/NoImpactWarning' import { useTradeConfirmState } from '../../hooks/useTradeConfirmState' import { PriceUpdatedBanner } from '../PriceUpdatedBanner' @@ -47,7 +48,7 @@ export interface TradeConfirmationProps { isPriceStatic?: boolean recipient?: string | null buttonText?: React.ReactNode - children?: ReactElement | ((restContent: ReactElement) => ReactElement) + children?: (restContent: ReactElement) => ReactElement } export function TradeConfirmation(props: TradeConfirmationProps) { @@ -151,13 +152,11 @@ export function TradeConfirmation(props: TradeConfirmationProps) { priceImpactParams={priceImpact} /> - {typeof children === 'function' ? ( - children(hookDetailsElement) - ) : ( + {children?.( <> - {children} {hookDetailsElement} - + + , )} {showRecipientWarning && } diff --git a/apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx b/apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx similarity index 100% rename from apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx rename to apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx diff --git a/apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx b/apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx similarity index 94% rename from apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx rename to apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx index 8725c58e99..333ef50ab3 100644 --- a/apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx @@ -4,7 +4,7 @@ import { HashLink } from 'react-router-hash-link' import styled from 'styled-components/macro' import { Nullish } from 'types' -import { WarningCard } from '../WarningCard' +import { WarningCard } from 'common/pure/WarningCard' const Link = styled(HashLink)` text-decoration: underline; diff --git a/apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/index.tsx similarity index 100% rename from apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/index.tsx rename to apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/index.tsx diff --git a/apps/cowswap-frontend/src/modules/trade/state/derivedTradeStateAtom.ts b/apps/cowswap-frontend/src/modules/trade/state/derivedTradeStateAtom.ts index a72ff94d9a..17414929b0 100644 --- a/apps/cowswap-frontend/src/modules/trade/state/derivedTradeStateAtom.ts +++ b/apps/cowswap-frontend/src/modules/trade/state/derivedTradeStateAtom.ts @@ -3,6 +3,7 @@ import { atom } from 'jotai' import { advancedOrdersDerivedStateAtom } from 'modules/advancedOrders' import { limitOrdersDerivedStateAtom } from 'modules/limitOrders' import { swapDerivedStateAtom } from 'modules/swap' +import { yieldDerivedStateAtom } from 'modules/yield' import { tradeTypeAtom } from './tradeTypeAtom' @@ -21,5 +22,9 @@ export const derivedTradeStateAtom = atom((get) => { return get(advancedOrdersDerivedStateAtom) } + if (tradeTypeInfo.tradeType === TradeType.YIELD) { + return get(yieldDerivedStateAtom) + } + return get(limitOrdersDerivedStateAtom) }) diff --git a/apps/cowswap-frontend/src/modules/swap/state/isEoaEthFlowAtom.ts b/apps/cowswap-frontend/src/modules/trade/state/isEoaEthFlowAtom.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/state/isEoaEthFlowAtom.ts rename to apps/cowswap-frontend/src/modules/trade/state/isEoaEthFlowAtom.ts diff --git a/apps/cowswap-frontend/src/modules/trade/types/TradeType.ts b/apps/cowswap-frontend/src/modules/trade/types/TradeType.ts index 0fdcfe3556..283e8ba206 100644 --- a/apps/cowswap-frontend/src/modules/trade/types/TradeType.ts +++ b/apps/cowswap-frontend/src/modules/trade/types/TradeType.ts @@ -4,6 +4,7 @@ export enum TradeType { SWAP = 'SWAP', LIMIT_ORDER = 'LIMIT_ORDER', ADVANCED_ORDERS = 'ADVANCED_ORDERS', + YIELD = 'YIELD', } export interface TradeTypeInfo { diff --git a/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeRoute.ts b/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeRoute.ts index 6a29a001b8..fc893ef326 100644 --- a/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeRoute.ts +++ b/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeRoute.ts @@ -11,7 +11,7 @@ import { RoutesValues } from 'common/constants/routes' export function parameterizeTradeRoute( { chainId, orderKind, inputCurrencyId, outputCurrencyId, inputCurrencyAmount, outputCurrencyAmount }: TradeUrlParams, route: RoutesValues, - withAmounts?: boolean + withAmounts?: boolean, ): string { const path = route .replace('/:chainId?', chainId ? `/${encodeURIComponent(chainId)}` : '') @@ -36,3 +36,10 @@ export function parameterizeTradeRoute( return path } + +export function addChainIdToRoute(route: RoutesValues, chainId: string | undefined): string { + return route + .replace('/:chainId?', chainId ? `/${encodeURIComponent(chainId)}` : '') + .replace('/:inputCurrencyId?', '') + .replace('/:outputCurrencyId?', '') +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts new file mode 100644 index 0000000000..7feb7896b8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts @@ -0,0 +1,56 @@ +import { useCallback } from 'react' + +import { useTradePriceImpact } from 'modules/trade' +import { logTradeFlow } from 'modules/trade/utils/logger' + +import { useConfirmPriceImpactWithoutFee } from 'common/hooks/useConfirmPriceImpactWithoutFee' + +import { useSafeBundleFlowContext } from './useSafeBundleFlowContext' +import { TradeFlowParams, useTradeFlowContext } from './useTradeFlowContext' +import { useTradeFlowType } from './useTradeFlowType' + +import { safeBundleApprovalFlow, safeBundleEthFlow } from '../services/safeBundleFlow' +import { swapFlow } from '../services/swapFlow' +import { FlowType } from '../types/TradeFlowContext' + +export function useHandleSwap(params: TradeFlowParams) { + const tradeFlowType = useTradeFlowType() + const tradeFlowContext = useTradeFlowContext(params) + const safeBundleFlowContext = useSafeBundleFlowContext() + const { confirmPriceImpactWithoutFee } = useConfirmPriceImpactWithoutFee() + const priceImpactParams = useTradePriceImpact() + + const contextIsReady = + Boolean( + [FlowType.SAFE_BUNDLE_ETH, FlowType.SAFE_BUNDLE_APPROVAL].includes(tradeFlowType) + ? safeBundleFlowContext + : tradeFlowContext, + ) && !!tradeFlowContext + + const callback = useCallback(async () => { + if (!tradeFlowContext) return + + if (tradeFlowType === FlowType.SAFE_BUNDLE_APPROVAL) { + if (!safeBundleFlowContext) throw new Error('Safe bundle flow context is not ready') + + logTradeFlow('SAFE BUNDLE APPROVAL FLOW', 'Start safe bundle approval flow') + return safeBundleApprovalFlow( + tradeFlowContext, + safeBundleFlowContext, + priceImpactParams, + confirmPriceImpactWithoutFee, + ) + } + if (tradeFlowType === FlowType.SAFE_BUNDLE_ETH) { + if (!safeBundleFlowContext) throw new Error('Safe bundle flow context is not ready') + + logTradeFlow('SAFE BUNDLE ETH FLOW', 'Start safe bundle eth flow') + return safeBundleEthFlow(tradeFlowContext, safeBundleFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) + } + + logTradeFlow('SWAP FLOW', 'Start swap flow') + return swapFlow(tradeFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) + }, [tradeFlowType, tradeFlowContext, safeBundleFlowContext, priceImpactParams, confirmPriceImpactWithoutFee]) + + return { callback, contextIsReady } +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts new file mode 100644 index 0000000000..5a83744dfa --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts @@ -0,0 +1,47 @@ +import { useMemo } from 'react' + +import { getCurrencyAddress } from '@cowprotocol/common-utils' +import { useSafeAppsSdk } from '@cowprotocol/wallet' + +import useSWR from 'swr' + +import { useReceiveAmountInfo } from 'modules/trade' + +import { useGP2SettlementContract, useTokenContract, useWETHContract } from 'common/hooks/useContract' +import { useNeedsApproval } from 'common/hooks/useNeedsApproval' +import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress' + +import { SafeBundleFlowContext } from '../types/TradeFlowContext' + +export function useSafeBundleFlowContext(): SafeBundleFlowContext | null { + const settlementContract = useGP2SettlementContract() + const spender = useTradeSpenderAddress() + + const safeAppsSdk = useSafeAppsSdk() + const wrappedNativeContract = useWETHContract() + const receiveAmountInfo = useReceiveAmountInfo() + const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount + const needsApproval = useNeedsApproval(inputAmountWithSlippage) + const inputCurrencyAddress = useMemo(() => { + return inputAmountWithSlippage ? getCurrencyAddress(inputAmountWithSlippage.currency) : undefined + }, [inputAmountWithSlippage]) + const erc20Contract = useTokenContract(inputCurrencyAddress) + + return ( + useSWR( + settlementContract && spender && safeAppsSdk && wrappedNativeContract && erc20Contract + ? [settlementContract, spender, safeAppsSdk, wrappedNativeContract, needsApproval, erc20Contract] + : null, + ([settlementContract, spender, safeAppsSdk, wrappedNativeContract, needsApproval, erc20Contract]) => { + return { + settlementContract, + spender, + safeAppsSdk, + wrappedNativeContract, + needsApproval, + erc20Contract, + } + }, + ).data || null + ) +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts new file mode 100644 index 0000000000..e080f6d985 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts @@ -0,0 +1,192 @@ +import { TokenWithLogo } from '@cowprotocol/common-const' +import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, OrderClass, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' +import { UiOrderType } from '@cowprotocol/types' +import { useIsSafeWallet, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' +import { useWalletProvider } from '@cowprotocol/wallet-provider' + +import { useDispatch } from 'react-redux' +import useSWR from 'swr' + +import { AppDispatch } from 'legacy/state' +import { useCloseModals } from 'legacy/state/application/hooks' + +import { useAppData, useAppDataHooks } from 'modules/appData' +import { useGeneratePermitHook, useGetCachedPermit, usePermitInfo } from 'modules/permit' +import { useEnoughBalanceAndAllowance } from 'modules/tokens' +import { TradeType, useDerivedTradeState, useReceiveAmountInfo, useTradeConfirmActions } from 'modules/trade' +import { getOrderValidTo, useTradeQuote } from 'modules/tradeQuote' + +import { useGP2SettlementContract } from 'common/hooks/useContract' + +import { TradeFlowContext } from '../types/TradeFlowContext' + +export interface TradeFlowParams { + deadline: number +} + +export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowContext | null { + const { chainId, account } = useWalletInfo() + const provider = useWalletProvider() + const { allowsOffchainSigning } = useWalletDetails() + const isSafeWallet = useIsSafeWallet() + const derivedTradeState = useDerivedTradeState() + const receiveAmountInfo = useReceiveAmountInfo() + + const sellCurrency = derivedTradeState?.inputCurrency + const inputAmount = receiveAmountInfo?.afterNetworkCosts.sellAmount + const outputAmount = receiveAmountInfo?.afterSlippage.buyAmount + const sellAmountBeforeFee = receiveAmountInfo?.afterNetworkCosts.sellAmount + const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount + const networkFee = receiveAmountInfo?.costs.networkFee.amountInSellCurrency + + const permitInfo = usePermitInfo(sellCurrency, TradeType.YIELD) + const generatePermitHook = useGeneratePermitHook() + const getCachedPermit = useGetCachedPermit() + const closeModals = useCloseModals() + const dispatch = useDispatch() + const tradeConfirmActions = useTradeConfirmActions() + const settlementContract = useGP2SettlementContract() + const appData = useAppData() + const typedHooks = useAppDataHooks() + const tradeQuote = useTradeQuote() + + const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId || SupportedChainId.MAINNET] + const { enoughAllowance } = useEnoughBalanceAndAllowance({ + account, + amount: inputAmountWithSlippage, + checkAllowanceAddress, + }) + + const { inputCurrency: sellToken, outputCurrency: buyToken, recipient, recipientAddress } = derivedTradeState || {} + const quoteParams = tradeQuote?.quoteParams + const quoteResponse = tradeQuote?.response + const localQuoteTimestamp = tradeQuote?.localQuoteTimestamp + + return ( + useSWR( + inputAmount && + outputAmount && + inputAmountWithSlippage && + sellAmountBeforeFee && + networkFee && + sellToken && + buyToken && + account && + provider && + appData && + quoteParams && + quoteResponse && + localQuoteTimestamp && + settlementContract + ? [ + account, + allowsOffchainSigning, + appData, + quoteParams, + quoteResponse, + localQuoteTimestamp, + buyToken, + chainId, + closeModals, + dispatch, + enoughAllowance, + generatePermitHook, + inputAmount, + inputAmountWithSlippage, + networkFee, + outputAmount, + permitInfo, + provider, + recipient, + sellAmountBeforeFee, + sellToken, + settlementContract, + tradeConfirmActions, + typedHooks, + deadline, + ] + : null, + ([ + account, + allowsOffchainSigning, + appData, + quoteParams, + quoteResponse, + localQuoteTimestamp, + buyToken, + chainId, + closeModals, + dispatch, + enoughAllowance, + generatePermitHook, + inputAmount, + inputAmountWithSlippage, + networkFee, + outputAmount, + permitInfo, + provider, + recipient, + sellAmountBeforeFee, + sellToken, + settlementContract, + tradeConfirmActions, + typedHooks, + deadline, + ]) => { + return { + context: { + chainId, + inputAmount, + outputAmount, + inputAmountWithSlippage, + }, + flags: { + allowsOffchainSigning, + }, + callbacks: { + closeModals, + getCachedPermit, + dispatch, + }, + tradeConfirmActions, + swapFlowAnalyticsContext: { + account, + recipient, + recipientAddress, + marketLabel: [inputAmount?.currency.symbol, outputAmount?.currency.symbol].join(','), + orderType: UiOrderType.YIELD, + }, + contract: settlementContract, + permitInfo: !enoughAllowance ? permitInfo : undefined, + generatePermitHook, + typedHooks, + orderParams: { + account, + chainId, + signer: provider.getSigner(), + kind: OrderKind.SELL, + inputAmount, + outputAmount, + sellAmountBeforeFee, + feeAmount: networkFee, + sellToken: sellToken as TokenWithLogo, + buyToken: buyToken as TokenWithLogo, + validTo: getOrderValidTo(deadline, { + validFor: quoteParams.validFor, + quoteValidTo: quoteResponse.quote.validTo, + localQuoteTimestamp, + }), + recipient: recipient || account, + recipientAddressOrName: recipient || null, + allowsOffchainSigning, + appData, + class: OrderClass.MARKET, + partiallyFillable: true, + quoteId: quoteResponse.id, + isSafeWallet, + }, + } + }, + ).data || null + ) +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowType.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowType.ts new file mode 100644 index 0000000000..3f8b847dd8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowType.ts @@ -0,0 +1,31 @@ +import { useIsEoaEthFlow, useIsSafeEthFlow, useReceiveAmountInfo } from 'modules/trade' + +import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' + +import { FlowType } from '../types/TradeFlowContext' + +export function useTradeFlowType(): FlowType { + const isEoaEthFlow = useIsEoaEthFlow() + const isSafeEthFlow = useIsSafeEthFlow() + const receiveAmountInfo = useReceiveAmountInfo() + const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount + + const isSafeBundle = useIsSafeApprovalBundle(inputAmountWithSlippage) + return getFlowType(isSafeBundle, isEoaEthFlow, isSafeEthFlow) +} + +function getFlowType(isSafeBundle: boolean, isEoaEthFlow: boolean, isSafeEthFlow: boolean): FlowType { + if (isSafeEthFlow) { + // Takes precedence over bundle approval + return FlowType.SAFE_BUNDLE_ETH + } + if (isSafeBundle) { + // Takes precedence over eth flow + return FlowType.SAFE_BUNDLE_APPROVAL + } + if (isEoaEthFlow) { + // Takes precedence over regular flow + return FlowType.EOA_ETH_FLOW + } + return FlowType.REGULAR +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/index.ts new file mode 100644 index 0000000000..ee07046f0d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/index.ts @@ -0,0 +1,4 @@ +export { useHandleSwap } from './hooks/useHandleSwap' +export { useTradeFlowContext } from './hooks/useTradeFlowContext' +export { useTradeFlowType } from './hooks/useTradeFlowType' +export * from './types/TradeFlowContext' diff --git a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/index.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/index.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/index.ts diff --git a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleApprovalFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts similarity index 88% rename from apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleApprovalFlow.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts index c31a835586..dd64fcdbc0 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleApprovalFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts @@ -11,17 +11,19 @@ import { buildApproveTx } from 'modules/operations/bundle/buildApproveTx' import { buildPresignTx } from 'modules/operations/bundle/buildPresignTx' import { buildZeroApproveTx } from 'modules/operations/bundle/buildZeroApproveTx' import { emitPostedOrderEvent } from 'modules/orders' -import { SafeBundleApprovalFlowContext } from 'modules/swap/services/types' import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep' import { logTradeFlow } from 'modules/trade/utils/logger' import { getSwapErrorMessage } from 'modules/trade/utils/swapErrorHelper' import { tradeFlowAnalytics } from 'modules/trade/utils/tradeFlowAnalytics' import { shouldZeroApprove as shouldZeroApproveFn } from 'modules/zeroApproval' +import { SafeBundleFlowContext, TradeFlowContext } from '../../types/TradeFlowContext' + const LOG_PREFIX = 'SAFE APPROVAL BUNDLE FLOW' export async function safeBundleApprovalFlow( - input: SafeBundleApprovalFlowContext, + tradeContext: TradeFlowContext, + safeBundleContext: SafeBundleFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, ): Promise { @@ -31,19 +33,9 @@ export async function safeBundleApprovalFlow( return false } - const { - erc20Contract, - spender, - context, - callbacks, - dispatch, - orderParams, - settlementContract, - safeAppsSdk, - swapFlowAnalyticsContext, - tradeConfirmActions, - typedHooks, - } = input + const { context, callbacks, orderParams, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext + + const { spender, settlementContract, safeAppsSdk, erc20Contract } = safeBundleContext const { chainId } = context const { account, isSafeWallet, recipientAddressOrName, inputAmount, outputAmount, kind } = orderParams @@ -59,7 +51,7 @@ export async function safeBundleApprovalFlow( const approveTx = await buildApproveTx({ erc20Contract, spender, - amountToApprove: context.trade.inputAmount, + amountToApprove: context.inputAmount, }) orderParams.appData = await removePermitHookFromAppData(orderParams.appData, typedHooks) @@ -79,7 +71,7 @@ export async function safeBundleApprovalFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) logTradeFlow(LOG_PREFIX, 'STEP 4: build presign tx') @@ -94,7 +86,7 @@ export async function safeBundleApprovalFlow( const shouldZeroApprove = await shouldZeroApproveFn({ tokenContract: erc20Contract, spender, - amountToApprove: context.trade.inputAmount, + amountToApprove: context.inputAmount, isBundle: true, }) @@ -102,7 +94,7 @@ export async function safeBundleApprovalFlow( const zeroApproveTx = await buildZeroApproveTx({ erc20Contract, spender, - currency: context.trade.inputAmount.currency, + currency: context.inputAmount.currency, }) safeTransactionData.unshift({ to: zeroApproveTx.to!, @@ -137,7 +129,7 @@ export async function safeBundleApprovalFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) tradeFlowAnalytics.sign(swapFlowAnalyticsContext) diff --git a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleEthFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts similarity index 89% rename from apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleEthFlow.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts index 16377662e1..0b6fe9277b 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts @@ -12,16 +12,18 @@ import { buildApproveTx } from 'modules/operations/bundle/buildApproveTx' import { buildPresignTx } from 'modules/operations/bundle/buildPresignTx' import { buildWrapTx } from 'modules/operations/bundle/buildWrapTx' import { emitPostedOrderEvent } from 'modules/orders' -import { SafeBundleEthFlowContext } from 'modules/swap/services/types' import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep' import { logTradeFlow } from 'modules/trade/utils/logger' import { getSwapErrorMessage } from 'modules/trade/utils/swapErrorHelper' import { tradeFlowAnalytics } from 'modules/trade/utils/tradeFlowAnalytics' +import { SafeBundleFlowContext, TradeFlowContext } from '../../types/TradeFlowContext' + const LOG_PREFIX = 'SAFE BUNDLE ETH FLOW' export async function safeBundleEthFlow( - input: SafeBundleEthFlowContext, + tradeContext: TradeFlowContext, + safeBundleContext: SafeBundleFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, ): Promise { @@ -31,27 +33,12 @@ export async function safeBundleEthFlow( return false } - const { - wrappedNativeContract, - needsApproval, - spender, - context, - callbacks, - dispatch, - orderParams, - settlementContract, - safeAppsSdk, - swapFlowAnalyticsContext, - tradeConfirmActions, - typedHooks, - } = input + const { context, callbacks, orderParams, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext + + const { spender, settlementContract, safeAppsSdk, needsApproval, wrappedNativeContract } = safeBundleContext const { account, recipientAddressOrName, kind } = orderParams - const { - inputAmountWithSlippage, - chainId, - trade: { inputAmount, outputAmount }, - } = context + const { inputAmountWithSlippage, chainId, inputAmount, outputAmount } = context tradeFlowAnalytics.wrapApproveAndPresign(swapFlowAnalyticsContext) const nativeAmountInWei = inputAmountWithSlippage.quotient.toString() @@ -107,7 +94,7 @@ export async function safeBundleEthFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) logTradeFlow(LOG_PREFIX, 'STEP 5: build presign tx') @@ -148,7 +135,7 @@ export async function safeBundleEthFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) tradeFlowAnalytics.sign(swapFlowAnalyticsContext) diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/README.md b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/README.md similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/services/swapFlow/README.md rename to apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/README.md diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts similarity index 92% rename from apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts index 405bb7367a..afef3f3941 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts @@ -17,10 +17,10 @@ import { tradeFlowAnalytics } from 'modules/trade/utils/tradeFlowAnalytics' import { presignOrderStep } from './steps/presignOrderStep' -import { SwapFlowContext } from '../types' +import { TradeFlowContext } from '../../types/TradeFlowContext' export async function swapFlow( - input: SwapFlowContext, + input: TradeFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, ): Promise { @@ -30,9 +30,7 @@ export async function swapFlow( } = input const { - context: { - trade: { inputAmount, outputAmount }, - }, + context: { inputAmount, outputAmount }, typedHooks, } = input const tradeAmounts = { inputAmount, outputAmount } @@ -42,9 +40,9 @@ export async function swapFlow( return false } - const { orderParams, context, permitInfo, generatePermitHook, swapFlowAnalyticsContext, callbacks, dispatch } = input - const { chainId, trade } = context - const inputCurrency = trade.inputAmount.currency + const { orderParams, context, permitInfo, generatePermitHook, swapFlowAnalyticsContext, callbacks } = input + const { chainId } = context + const inputCurrency = inputAmount.currency const cachedPermit = await getCachedPermit(getAddress(inputCurrency)) try { @@ -88,7 +86,7 @@ export async function swapFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) logTradeFlow('SWAP FLOW', 'STEP 5: presign order (optional)') @@ -119,7 +117,7 @@ export async function swapFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) } diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/steps/presignOrderStep.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/steps/presignOrderStep.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/services/swapFlow/steps/presignOrderStep.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/steps/presignOrderStep.ts diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/swapFlow.puml b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/swapFlow.puml similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/services/swapFlow/swapFlow.puml rename to apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/swapFlow.puml diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts new file mode 100644 index 0000000000..69ea103b01 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts @@ -0,0 +1,52 @@ +import { Erc20, GPv2Settlement, Weth } from '@cowprotocol/abis' +import type { Command } from '@cowprotocol/types' +import type SafeAppsSDK from '@safe-global/safe-apps-sdk' +import type { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import type { AppDispatch } from 'legacy/state' +import type { PostOrderParams } from 'legacy/utils/trade' + +import type { TypedAppDataHooks } from 'modules/appData' +import type { GeneratePermitHook, IsTokenPermittableResult, useGetCachedPermit } from 'modules/permit' +import type { TradeConfirmActions } from 'modules/trade' +import type { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics' + +export enum FlowType { + REGULAR = 'REGULAR', + EOA_ETH_FLOW = 'EOA_ETH_FLOW', + SAFE_BUNDLE_APPROVAL = 'SAFE_BUNDLE_APPROVAL', + SAFE_BUNDLE_ETH = 'SAFE_BUNDLE_ETH', +} + +export interface TradeFlowContext { + context: { + chainId: number + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + inputAmountWithSlippage: CurrencyAmount + } + flags: { + allowsOffchainSigning: boolean + } + callbacks: { + closeModals: Command + getCachedPermit: ReturnType + dispatch: AppDispatch + } + tradeConfirmActions: TradeConfirmActions + swapFlowAnalyticsContext: TradeFlowAnalyticsContext + orderParams: PostOrderParams + contract: GPv2Settlement + permitInfo: IsTokenPermittableResult + generatePermitHook: GeneratePermitHook + typedHooks?: TypedAppDataHooks +} + +export interface SafeBundleFlowContext { + settlementContract: GPv2Settlement + spender: string + safeAppsSdk: SafeAppsSDK + wrappedNativeContract: Weth + needsApproval: boolean + erc20Contract: Erc20 +} diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts index 2b0656b1ca..c6f1713cb3 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts @@ -1,4 +1,5 @@ import { getIsNativeToken, isAddress, isFractionFalsy } from '@cowprotocol/common-utils' +import { PriceQuality } from '@cowprotocol/cow-sdk' import { TradeType } from 'modules/trade' import { isQuoteExpired } from 'modules/tradeQuote' @@ -79,6 +80,7 @@ export function validateTradeForm(context: TradeFormValidationContext): TradeFor if ( derivedTradeState.tradeType !== TradeType.LIMIT_ORDER && !tradeQuote.isLoading && + tradeQuote.quoteParams?.priceQuality !== PriceQuality.FAST && isQuoteExpired({ expirationDate: tradeQuote.response?.expiration, deadlineParams: { diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts index ac0677b301..47af867581 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts @@ -11,14 +11,14 @@ import { useUpdateTradeQuote } from './useUpdateTradeQuote' import { tradeQuoteParamsAtom } from '../state/tradeQuoteParamsAtom' -export function useSetTradeQuoteParams(amount: Nullish>) { +export function useSetTradeQuoteParams(amount: Nullish>, fastQuote?: boolean) { const updateTradeQuote = useUpdateTradeQuote() const updateState = useSetAtom(tradeQuoteParamsAtom) - const context = useSafeMemoObject({ amount, updateTradeQuote, updateState }) + const context = useSafeMemoObject({ amount, fastQuote, updateTradeQuote, updateState }) useEffect(() => { context.updateTradeQuote({ response: null, error: null }) - context.updateState({ amount: context.amount || null }) + context.updateState({ amount: context.amount || null, fastQuote: context.fastQuote }) }, [context]) } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index 769e40e219..e2f3d301c5 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -3,7 +3,7 @@ import { useLayoutEffect, useMemo } from 'react' import { useDebounce } from '@cowprotocol/common-hooks' import { onlyResolvesLast } from '@cowprotocol/common-utils' -import { OrderQuoteResponse } from '@cowprotocol/cow-sdk' +import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' import { useAreUnsupportedTokens } from '@cowprotocol/tokens' import ms from 'ms.macro' @@ -23,13 +23,14 @@ export const PRICE_UPDATE_INTERVAL = ms`30s` const AMOUNT_CHANGE_DEBOUNCE_TIME = ms`300` // Solves the problem of multiple requests -const getQuoteOnlyResolveLast = onlyResolvesLast(getQuote) +const getFastQuote = onlyResolvesLast(getQuote) +const getOptimalQuote = onlyResolvesLast(getQuote) export function useTradeQuotePolling() { - const { amount } = useAtomValue(tradeQuoteParamsAtom) + const { amount, fastQuote } = useAtomValue(tradeQuoteParamsAtom) const amountStr = useDebounce( useMemo(() => amount?.quotient.toString() || null, [amount]), - AMOUNT_CHANGE_DEBOUNCE_TIME + AMOUNT_CHANGE_DEBOUNCE_TIME, ) const quoteParams = useQuoteParams(amountStr) @@ -51,10 +52,14 @@ export function useTradeQuotePolling() { return } - const fetchQuote = (hasParamsChanged: boolean) => { + const fetchQuote = (hasParamsChanged: boolean, priceQuality: PriceQuality, fetchStartTimestamp: number) => { updateQuoteState({ isLoading: true, hasParamsChanged }) - getQuoteOnlyResolveLast(quoteParams) + const isOptimalQuote = priceQuality === PriceQuality.OPTIMAL + const requestParams = { ...quoteParams, priceQuality } + const request = isOptimalQuote ? getOptimalQuote(requestParams) : getFastQuote(requestParams) + + return request .then((response) => { const { cancelled, data } = response @@ -62,24 +67,44 @@ export function useTradeQuotePolling() { return } - updateQuoteState({ response: data, quoteParams, isLoading: false, error: null, hasParamsChanged: false }) + updateQuoteState({ + response: data, + quoteParams: requestParams, + ...(isOptimalQuote ? { isLoading: false } : null), + error: null, + hasParamsChanged: false, + fetchStartTimestamp, + }) }) .catch((error: QuoteApiError) => { console.log('[useGetQuote]:: fetchQuote error', error) updateQuoteState({ isLoading: false, error, hasParamsChanged: false }) if (error.type === QuoteApiErrorCodes.UnsupportedToken) { - processUnsupportedTokenError(error, quoteParams) + processUnsupportedTokenError(error, requestParams) } }) } - fetchQuote(true) + const fetchStartTimestamp = Date.now() + if (fastQuote) fetchQuote(true, PriceQuality.FAST, fetchStartTimestamp) + fetchQuote(true, PriceQuality.OPTIMAL, fetchStartTimestamp) - const intervalId = setInterval(() => fetchQuote(false), PRICE_UPDATE_INTERVAL) + const intervalId = setInterval(() => { + const fetchStartTimestamp = Date.now() + if (fastQuote) fetchQuote(false, PriceQuality.FAST, fetchStartTimestamp) + fetchQuote(false, PriceQuality.OPTIMAL, fetchStartTimestamp) + }, PRICE_UPDATE_INTERVAL) return () => clearInterval(intervalId) - }, [quoteParams, updateQuoteState, updateCurrencyAmount, processUnsupportedTokenError, getIsUnsupportedTokens]) + }, [ + fastQuote, + quoteParams, + updateQuoteState, + updateCurrencyAmount, + processUnsupportedTokenError, + getIsUnsupportedTokens, + ]) return null } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts index b4e84e580e..0929541273 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts @@ -1,6 +1,6 @@ import { atom } from 'jotai' -import { OrderQuoteResponse } from '@cowprotocol/cow-sdk' +import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' import type { LegacyFeeQuoteParams } from 'legacy/state/price/types' @@ -12,6 +12,7 @@ export interface TradeQuoteState { isLoading: boolean hasParamsChanged: boolean quoteParams: LegacyFeeQuoteParams | null + fetchStartTimestamp: number | null localQuoteTimestamp: number | null } @@ -21,6 +22,7 @@ export const DEFAULT_TRADE_QUOTE_STATE: TradeQuoteState = { isLoading: false, hasParamsChanged: false, quoteParams: null, + fetchStartTimestamp: null, localQuoteTimestamp: null, } @@ -30,6 +32,15 @@ export const updateTradeQuoteAtom = atom(null, (get, set, nextState: Partial { const prevState = get(tradeQuoteAtom) + // Don't update state if Fast quote finished after Optimal quote + if ( + prevState.fetchStartTimestamp === nextState.fetchStartTimestamp && + nextState.response && + nextState.quoteParams?.priceQuality === PriceQuality.FAST + ) { + return { ...prevState } + } + return { ...prevState, ...nextState, diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteParamsAtom.ts b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteParamsAtom.ts index dd5e54de55..ae1193ec84 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteParamsAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteParamsAtom.ts @@ -4,6 +4,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' export interface TradeQuoteParamsState { amount: CurrencyAmount | null + fastQuote?: boolean } export const tradeQuoteParamsAtom = atom({ amount: null }) diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx b/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx new file mode 100644 index 0000000000..225e0b99f9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx @@ -0,0 +1,39 @@ +import { percentToBps } from '@cowprotocol/common-utils' +import { BannerOrientation, InfoTooltip, InlineBanner } from '@cowprotocol/ui' +import { useWalletInfo } from '@cowprotocol/wallet' + +import styled from 'styled-components/macro' + +import { useIsSmartSlippageApplied, useTradeSlippage } from 'modules/tradeSlippage' + +const StyledInlineBanner = styled(InlineBanner)` + text-align: center; +` + +export type HighSuggestedSlippageWarningProps = { + isTradePriceUpdating: boolean +} + +export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarningProps) { + const { isTradePriceUpdating } = props + const { account } = useWalletInfo() + const slippage = useTradeSlippage() + + const isSmartSlippageApplied = useIsSmartSlippageApplied() + const isSuggestedSlippage = isSmartSlippageApplied && !isTradePriceUpdating && !!account + const slippageBps = percentToBps(slippage) + + if (!isSuggestedSlippage || !slippageBps || slippageBps <= 200) { + return null + } + + return ( + + Slippage adjusted to {`${slippageBps / 100}`}% to ensure quick execution + + + ) +} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSlippageModified.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSlippageModified.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsSlippageModified.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSlippageModified.ts diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSmartSlippageApplied.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSmartSlippageApplied.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsSmartSlippageApplied.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSmartSlippageApplied.ts diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useSetSlippage.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useSetSlippage.ts new file mode 100644 index 0000000000..3040c569c6 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useSetSlippage.ts @@ -0,0 +1,7 @@ +import { useSetAtom } from 'jotai' + +import { setTradeSlippageAtom } from '../state/slippageValueAndTypeAtom' + +export function useSetSlippage() { + return useSetAtom(setTradeSlippageAtom) +} diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useTradeSlippage.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useTradeSlippage.ts new file mode 100644 index 0000000000..e3666cbc36 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useTradeSlippage.ts @@ -0,0 +1,22 @@ +import { useAtomValue } from 'jotai/index' + +import { bpsToPercent } from '@cowprotocol/common-utils' +import { Percent } from '@uniswap/sdk-core' + +import { + defaultSlippageAtom, + smartTradeSlippageAtom, + tradeSlippagePercentAtom, +} from '../state/slippageValueAndTypeAtom' + +export function useTradeSlippage(): Percent { + return useAtomValue(tradeSlippagePercentAtom) +} + +export function useDefaultTradeSlippage() { + return bpsToPercent(useAtomValue(defaultSlippageAtom)) +} + +export function useSmartTradeSlippage() { + return useAtomValue(smartTradeSlippageAtom) +} diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/index.tsx b/apps/cowswap-frontend/src/modules/tradeSlippage/index.tsx new file mode 100644 index 0000000000..35f3926515 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/index.tsx @@ -0,0 +1,6 @@ +export { SmartSlippageUpdater } from './updaters/SmartSlippageUpdater' +export { HighSuggestedSlippageWarning } from './containers/HighSuggestedSlippageWarning' +export * from './hooks/useSetSlippage' +export * from './hooks/useTradeSlippage' +export * from './hooks/useIsSmartSlippageApplied' +export * from './hooks/useIsSlippageModified' diff --git a/apps/cowswap-frontend/src/modules/swap/state/slippageValueAndTypeAtom.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts similarity index 67% rename from apps/cowswap-frontend/src/modules/swap/state/slippageValueAndTypeAtom.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts index c7351bcafd..f1d081e351 100644 --- a/apps/cowswap-frontend/src/modules/swap/state/slippageValueAndTypeAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts @@ -6,16 +6,21 @@ import { bpsToPercent } from '@cowprotocol/common-utils' import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { walletInfoAtom } from '@cowprotocol/wallet' -import { isEoaEthFlowAtom } from './isEoaEthFlowAtom' +import { isEoaEthFlowAtom } from 'modules/trade' type SlippageBpsPerNetwork = Record type SlippageType = 'smart' | 'default' | 'user' -const normalSwapSlippageAtom = atomWithStorage('swapSlippageAtom:v0', mapSupportedNetworks(null)) +const normalTradeSlippageAtom = atomWithStorage( + 'swapSlippageAtom:v0', + mapSupportedNetworks(null), +) const ethFlowSlippageAtom = atomWithStorage('ethFlowSlippageAtom:v0', mapSupportedNetworks(null)) +export const smartTradeSlippageAtom = atom(null) + export const defaultSlippageAtom = atom((get) => { const { chainId } = get(walletInfoAtom) const isEoaEthFlow = get(isEoaEthFlowAtom) @@ -26,41 +31,42 @@ export const defaultSlippageAtom = atom((get) => { const currentSlippageAtom = atom((get) => { const { chainId } = get(walletInfoAtom) const isEoaEthFlow = get(isEoaEthFlowAtom) - const normalSwapSlippage = get(normalSwapSlippageAtom) + const normalSlippage = get(normalTradeSlippageAtom) const ethFlowSlippage = get(ethFlowSlippageAtom) - return (isEoaEthFlow ? ethFlowSlippage : normalSwapSlippage)?.[chainId] ?? null + return (isEoaEthFlow ? ethFlowSlippage : normalSlippage)?.[chainId] ?? null }) -export const smartSwapSlippageAtom = atom(null) - export const slippageValueAndTypeAtom = atom<{ type: SlippageType; value: number }>((get) => { const currentSlippage = get(currentSlippageAtom) const defaultSlippage = get(defaultSlippageAtom) - const smartSwapSlippage = get(smartSwapSlippageAtom) + const smartSlippage = get(smartTradeSlippageAtom) const isEoaEthFlow = get(isEoaEthFlowAtom) if (typeof currentSlippage === 'number') { return { type: 'user', value: currentSlippage } } - if (!isEoaEthFlow && smartSwapSlippage && smartSwapSlippage !== defaultSlippage) { - return { type: 'smart', value: smartSwapSlippage } + if (!isEoaEthFlow && smartSlippage && smartSlippage !== defaultSlippage) { + return { type: 'smart', value: smartSlippage } } return { type: 'default', value: defaultSlippage } }) -export const swapSlippagePercentAtom = atom((get) => { +export const tradeSlippagePercentAtom = atom((get) => { return bpsToPercent(get(slippageValueAndTypeAtom).value) }) -export const setSwapSlippageAtom = atom(null, (get, set, slippageBps: number | null) => { +export const setTradeSlippageAtom = atom(null, (get, set, slippageBps: number | null) => { const { chainId } = get(walletInfoAtom) const isEoaEthFlow = get(isEoaEthFlowAtom) - const currentStateAtom = isEoaEthFlow ? ethFlowSlippageAtom : normalSwapSlippageAtom + const currentStateAtom = isEoaEthFlow ? ethFlowSlippageAtom : normalTradeSlippageAtom const currentState = get(currentStateAtom) - set(currentStateAtom, { ...currentState, [chainId]: slippageBps }) + set(currentStateAtom, { + ...currentState, + [chainId]: slippageBps, + }) }) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/index.ts similarity index 88% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/index.ts index 73e6173e77..88efcdae6a 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/index.ts @@ -6,13 +6,13 @@ import { useTradeConfirmState } from 'modules/trade' import { useSmartSlippageFromBff } from './useSmartSlippageFromBff' import { useSmartSlippageFromFeeMultiplier } from './useSmartSlippageFromFeeMultiplier' -import { smartSwapSlippageAtom } from '../../state/slippageValueAndTypeAtom' +import { smartTradeSlippageAtom } from '../../state/slippageValueAndTypeAtom' const MAX_BPS = 500 // 5% const MIN_BPS = 50 // 0.5% export function SmartSlippageUpdater() { - const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) + const setSmartSwapSlippage = useSetAtom(smartTradeSlippageAtom) const bffSlippageBps = useSmartSlippageFromBff() const feeMultiplierSlippageBps = useSmartSlippageFromFeeMultiplier() diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx new file mode 100644 index 0000000000..dfc6ddcdd5 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx @@ -0,0 +1,29 @@ +import { InlineBanner } from '@cowprotocol/ui' +import { useIsBundlingSupported, useIsSmartContractWallet } from '@cowprotocol/wallet' + +import { useIsNativeIn, useWrappedToken } from 'modules/trade' + +import useNativeCurrency from 'lib/hooks/useNativeCurrency' + +export function BundleTxWrapBanner() { + const nativeCurrencySymbol = useNativeCurrency().symbol || 'ETH' + const wrappedCurrencySymbol = useWrappedToken().symbol || 'WETH' + + const isBundlingSupported = useIsBundlingSupported() + const isNativeIn = useIsNativeIn() + const isSmartContractWallet = useIsSmartContractWallet() + const showWrapBundlingBanner = Boolean(isNativeIn && isSmartContractWallet && isBundlingSupported) + + if (!showWrapBundlingBanner) return null + + return ( + + 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! +

+
+ ) +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/consts.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/consts.ts new file mode 100644 index 0000000000..6dc0eed2a7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/consts.ts @@ -0,0 +1,3 @@ +export const HIGH_TIER_FEE = 30 +export const MEDIUM_TIER_FEE = 20 +export const LOW_TIER_FEE = 10 diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts new file mode 100644 index 0000000000..158912718c --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts @@ -0,0 +1,86 @@ +import { atom, useAtom } from 'jotai' +import { useMemo } from 'react' + +import { FEE_SIZE_THRESHOLD } from '@cowprotocol/common-const' + +import { useReceiveAmountInfo } from 'modules/trade' + +import { useSafeEffect, useSafeMemo } from 'common/hooks/useSafeMemo' + +const feeWarningAcceptedAtom = atom(false) + +/** + * useHighFeeWarning + * @description checks whether fee vs trade inputAmount = high fee warning + * @description returns params related to high fee and a cb for checking/unchecking fee acceptance + */ +export function useHighFeeWarning() { + const receiveAmountInfo = useReceiveAmountInfo() + + const [feeWarningAccepted, setFeeWarningAccepted] = useAtom(feeWarningAcceptedAtom) + + // only considers inputAmount vs fee (fee is in input token) + const [isHighFee, feePercentage] = useMemo(() => { + if (!receiveAmountInfo) return [false, undefined] + + const { + isSell, + beforeNetworkCosts, + afterNetworkCosts, + costs: { networkFee, partnerFee }, + quotePrice, + } = receiveAmountInfo + + const outputAmountWithoutFee = isSell ? beforeNetworkCosts.buyAmount : afterNetworkCosts.buyAmount + + const inputAmountAfterFees = isSell ? beforeNetworkCosts.sellAmount : afterNetworkCosts.sellAmount + + const feeAsCurrency = isSell ? quotePrice.quote(networkFee.amountInSellCurrency) : networkFee.amountInSellCurrency + + const volumeFeeAmount = partnerFee.amount + + const totalFeeAmount = volumeFeeAmount ? feeAsCurrency.add(volumeFeeAmount) : feeAsCurrency + const targetAmount = isSell ? outputAmountWithoutFee : inputAmountAfterFees + const feePercentage = totalFeeAmount.divide(targetAmount).multiply(100).asFraction + + return [feePercentage.greaterThan(FEE_SIZE_THRESHOLD), feePercentage] + }, [receiveAmountInfo]) + + // reset the state when users change swap params + useSafeEffect(() => { + setFeeWarningAccepted(false) + }, [ + receiveAmountInfo?.beforeNetworkCosts.sellAmount.currency, + receiveAmountInfo?.beforeNetworkCosts.buyAmount.currency, + receiveAmountInfo?.isSell, + ]) + + return useSafeMemo( + () => ({ + isHighFee, + feePercentage, + // we only care/check about feeWarning being accepted if the fee is actually high.. + feeWarningAccepted: _computeFeeWarningAcceptedState({ feeWarningAccepted, isHighFee }), + setFeeWarningAccepted, + }), + [isHighFee, feePercentage, feeWarningAccepted, setFeeWarningAccepted], + ) +} + +function _computeFeeWarningAcceptedState({ + feeWarningAccepted, + isHighFee, +}: { + feeWarningAccepted: boolean + isHighFee: boolean +}) { + if (feeWarningAccepted) return true + else { + // is the fee high? that's only when we care + if (isHighFee) { + return feeWarningAccepted + } else { + return true + } + } +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/index.tsx new file mode 100644 index 0000000000..a68da1afc3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/index.tsx @@ -0,0 +1,89 @@ +import { useCallback } from 'react' + +import { HoverTooltip } from '@cowprotocol/ui' +import { useWalletInfo } from '@cowprotocol/wallet' +import { Fraction } from '@uniswap/sdk-core' + +import { AlertTriangle } from 'react-feather' + +import { useIsDarkMode } from 'legacy/state/user/hooks' + +import { useSafeMemo } from 'common/hooks/useSafeMemo' + +import { HIGH_TIER_FEE, LOW_TIER_FEE, MEDIUM_TIER_FEE } from './consts' +import { useHighFeeWarning } from './hooks/useHighFeeWarning' +import { ErrorStyledInfoIcon, WarningCheckboxContainer, WarningContainer } from './styled' + +interface HighFeeWarningProps { + readonlyMode?: boolean +} + +export function HighFeeWarning({ readonlyMode }: HighFeeWarningProps) { + const { account } = useWalletInfo() + const { feeWarningAccepted, setFeeWarningAccepted } = useHighFeeWarning() + const darkMode = useIsDarkMode() + + const toggleFeeWarningAccepted = useCallback(() => { + setFeeWarningAccepted((state) => !state) + }, [setFeeWarningAccepted]) + + const { isHighFee, feePercentage } = useHighFeeWarning() + const level = useSafeMemo(() => _getWarningInfo(feePercentage), [feePercentage]) + + if (!isHighFee) return null + + return ( + +
+ + Costs exceed {level}% of the swap amount!{' '} + }> + + {' '} +
+ + {account && !readonlyMode && ( + + {' '} + Swap anyway + + )} +
+ ) +} + +// checks fee as percentage (30% not a decimal) +function _getWarningInfo(feePercentage?: Fraction) { + if (!feePercentage || feePercentage.lessThan(LOW_TIER_FEE)) { + return undefined + } else if (feePercentage.lessThan(MEDIUM_TIER_FEE)) { + return LOW_TIER_FEE + } else if (feePercentage.lessThan(HIGH_TIER_FEE)) { + return MEDIUM_TIER_FEE + } else { + return HIGH_TIER_FEE + } +} + +const HighFeeWarningMessage = ({ feePercentage }: { feePercentage?: Fraction }) => ( +
+ + Current network costs make up{' '} + + {feePercentage?.toFixed(2)}% + {' '} + of your swap amount. +
+
+ Consider waiting for lower network costs. +
+
+ You may still move forward with this swap but a high percentage of it will be consumed by network costs. +
+
+) diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/styled.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/styled.tsx new file mode 100644 index 0000000000..8088eb926a --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/styled.tsx @@ -0,0 +1,139 @@ +import { Media, UI } from '@cowprotocol/ui' + +import { Info } from 'react-feather' +import styled from 'styled-components/macro' + +import { HIGH_TIER_FEE, LOW_TIER_FEE, MEDIUM_TIER_FEE } from './consts' + +interface HighFeeContainerProps { + level?: number + isDarkMode?: boolean +} + +// TODO: refactor these styles +export const AuxInformationContainer = styled.div<{ + margin?: string + borderColor?: string + borderWidth?: string + hideInput: boolean + disabled?: boolean + showAux?: boolean +}>` + border: 1px solid ${({ hideInput }) => (hideInput ? ' transparent' : `var(${UI.COLOR_PAPER_DARKER})`)}; + background-color: var(${UI.COLOR_PAPER}); + width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; + + :focus, + :hover { + border: 1px solid ${({ theme, hideInput }) => (hideInput ? ' transparent' : theme.background)}; + } + + ${({ theme, hideInput, disabled }) => + !disabled && + ` + :focus, + :hover { + border: 1px solid ${hideInput ? ' transparent' : theme.background}; + } + `} + + margin: ${({ margin = '0 auto' }) => margin}; + border-radius: 0 0 15px 15px; + border: 2px solid var(${UI.COLOR_PAPER_DARKER}); + + &:hover { + border: 2px solid var(${UI.COLOR_PAPER_DARKER}); + } + + ${Media.upToSmall()} { + height: auto; + flex-flow: column wrap; + justify-content: flex-end; + align-items: flex-end; + } +` + +export const WarningCheckboxContainer = styled.label` + display: flex; + width: 100%; + font-weight: bold; + gap: 2px; + justify-content: center; + align-items: center; + border-radius: 16px; + padding: 0; + margin: 10px auto; + cursor: pointer; + + > input { + cursor: pointer; + margin: 1px 4px 0 0; + } +` + +export const WarningContainer = styled(AuxInformationContainer).attrs((props) => ({ + ...props, + hideInput: true, +}))` + --warningColor: ${({ theme, level }) => + level === HIGH_TIER_FEE + ? theme.danger + : level === MEDIUM_TIER_FEE + ? theme.warning + : LOW_TIER_FEE + ? theme.alert + : theme.info}; + color: inherit; + padding: 16px; + width: 100%; + border-radius: 16px; + border: 0; + margin: ${({ margin = '0 auto' }) => margin}; + position: relative; + z-index: 1; + + &:hover { + border: 0; + } + + &::before { + content: ''; + display: block; + position: absolute; + top: 0; + left: 0; + border-radius: inherit; + background: var(--warningColor); + opacity: ${({ isDarkMode }) => (isDarkMode ? 0.2 : 0.15)}; + z-index: -1; + width: 100%; + height: 100%; + pointer-events: none; + } + + > div { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; + text-align: center; + + > svg:first-child { + stroke: var(--warningColor); + } + } +` + +export const ErrorStyledInfoIcon = styled(Info)` + opacity: 0.6; + line-height: 0; + vertical-align: middle; + transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; + + &:hover { + opacity: 1; + } + color: ${({ theme }) => (theme.darkMode ? '#FFCA4A' : '#564D00')}; +` diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowDeadline/index.tsx new file mode 100644 index 0000000000..2bc83f665d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowDeadline/index.tsx @@ -0,0 +1,30 @@ +import { useMemo } from 'react' + +import { useIsEoaEthFlow, useIsWrapOrUnwrap } from 'modules/trade' + +import useNativeCurrency from 'lib/hooks/useNativeCurrency' + +import { RowDeadlineContent } from '../../pure/Row/RowDeadline' + +export function RowDeadline({ deadline }: { deadline: number }) { + const isEoaEthFlow = useIsEoaEthFlow() + const nativeCurrency = useNativeCurrency() + const isWrapOrUnwrap = useIsWrapOrUnwrap() + + const props = useMemo(() => { + const displayDeadline = Math.floor(deadline / 60) + ' minutes' + return { + userDeadline: deadline, + symbols: [nativeCurrency.symbol], + displayDeadline, + isEoaEthFlow, + isWrapOrUnwrap, + } + }, [isEoaEthFlow, isWrapOrUnwrap, nativeCurrency.symbol, deadline]) + + if (!isEoaEthFlow || isWrapOrUnwrap) { + return null + } + + return +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx new file mode 100644 index 0000000000..99f75e014f --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx @@ -0,0 +1,66 @@ +import { useMemo } from 'react' + +import { formatPercent } from '@cowprotocol/common-utils' +import { useWalletInfo } from '@cowprotocol/wallet' +import { Percent } from '@uniswap/sdk-core' + +import { useIsEoaEthFlow } from 'modules/trade' +import { useIsSmartSlippageApplied, useSetSlippage, useSmartTradeSlippage } from 'modules/tradeSlippage' + +import useNativeCurrency from 'lib/hooks/useNativeCurrency' + +import { RowSlippageContent } from '../../pure/Row/RowSlippageContent' + +export interface RowSlippageProps { + allowedSlippage: Percent + slippageLabel?: React.ReactNode + slippageTooltip?: React.ReactNode + isSlippageModified: boolean + isTradePriceUpdating: boolean +} + +export function RowSlippage({ + allowedSlippage, + slippageTooltip, + slippageLabel, + isTradePriceUpdating, + isSlippageModified, +}: RowSlippageProps) { + const { chainId } = useWalletInfo() + + const isEoaEthFlow = useIsEoaEthFlow() + const nativeCurrency = useNativeCurrency() + const smartSlippage = useSmartTradeSlippage() + const isSmartSlippageApplied = useIsSmartSlippageApplied() + const setSlippage = useSetSlippage() + + const props = useMemo( + () => ({ + chainId, + isEoaEthFlow, + symbols: [nativeCurrency.symbol], + allowedSlippage, + slippageLabel, + slippageTooltip, + displaySlippage: `${formatPercent(allowedSlippage)}%`, + isSmartSlippageApplied, + isSmartSlippageLoading: isTradePriceUpdating, + smartSlippage: + smartSlippage && !isEoaEthFlow ? `${formatPercent(new Percent(smartSlippage, 10_000))}%` : undefined, + setAutoSlippage: smartSlippage && !isEoaEthFlow ? () => setSlippage(null) : undefined, + }), + [ + chainId, + isEoaEthFlow, + nativeCurrency.symbol, + allowedSlippage, + slippageLabel, + slippageTooltip, + smartSlippage, + isSmartSlippageApplied, + isTradePriceUpdating, + ], + ) + + return +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx new file mode 100644 index 0000000000..e43aba6dcb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx @@ -0,0 +1,116 @@ +import { useAtom } from 'jotai' +import { ReactElement, RefObject, useCallback, useEffect, useRef } from 'react' + +import { StatefulValue } from '@cowprotocol/types' +import { HelpTooltip, RowBetween, RowFixed } from '@cowprotocol/ui' + +import { Trans } from '@lingui/macro' +import { Menu, useMenuButtonContext } from '@reach/menu-button' +import { Text } from 'rebass' +import { ThemedText } from 'theme' + +import { AutoColumn } from 'legacy/components/Column' +import { Toggle } from 'legacy/components/Toggle' + +import { toggleRecipientAddressAnalytics } from 'modules/analytics' +import { SettingsIcon } from 'modules/trade/pure/Settings' + +import * as styledEl from './styled' + +import { settingsTabStateAtom } from '../../state/settingsTabState' +import { TransactionSettings } from '../TransactionSettings' + +interface SettingsTabProps { + className?: string + recipientToggleState: StatefulValue + deadlineState: StatefulValue +} + +export function SettingsTab({ className, recipientToggleState, deadlineState }: SettingsTabProps) { + const menuButtonRef = useRef(null) + + const [recipientToggleVisible, toggleRecipientVisibilityAux] = recipientToggleState + const toggleRecipientVisibility = useCallback( + (value?: boolean) => { + const isVisible = value ?? !recipientToggleVisible + toggleRecipientAddressAnalytics(isVisible) + toggleRecipientVisibilityAux(isVisible) + }, + [toggleRecipientVisibilityAux, recipientToggleVisible], + ) + + return ( + + + + + + + + + + Transaction Settings + + + + Interface Settings + + + + + + Custom Recipient + + + Allows you to choose a destination address for the swap other than the connected one. + + } + /> + + + + + + + + + ) +} + +interface SettingsTabControllerProps { + buttonRef: RefObject + children: ReactElement +} + +/** + * https://stackoverflow.com/questions/70596487/how-to-programmatically-expand-react-reach-ui-reach-menu-button-menu + */ +function SettingsTabController({ buttonRef, children }: SettingsTabControllerProps) { + const [settingsTabState, setSettingsTabState] = useAtom(settingsTabStateAtom) + const { isExpanded } = useMenuButtonContext() + + const toggleMenu = () => { + buttonRef.current?.dispatchEvent(new Event('mousedown', { bubbles: true })) + } + + useEffect(() => { + if (settingsTabState.open) { + toggleMenu() + } + }, [settingsTabState.open]) + + useEffect(() => { + if (settingsTabState.open && !isExpanded) { + toggleMenu() + setSettingsTabState({ open: false }) + } + }, [settingsTabState.open, isExpanded]) + + return children +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/styled.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/styled.tsx new file mode 100644 index 0000000000..0b63cc2c26 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/styled.tsx @@ -0,0 +1,86 @@ +import { Media, RowFixed, UI } from '@cowprotocol/ui' + +import { MenuButton, MenuList } from '@reach/menu-button' +import { transparentize } from 'color2k' +import styled from 'styled-components/macro' + +export const StyledMenuButton = styled(MenuButton)` + position: relative; + width: 100%; + border: none; + background-color: transparent; + margin: 0; + padding: 0; + border-radius: 0.5rem; + height: var(${UI.ICON_SIZE_NORMAL}); + opacity: 0.6; + transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; + color: inherit; + display: flex; + align-items: center; + + &:hover, + &:focus { + opacity: 1; + cursor: pointer; + outline: none; + color: currentColor; + } + + svg { + opacity: 1; + margin: auto; + transition: transform 0.3s cubic-bezier(0.65, 0.05, 0.36, 1); + color: inherit; + } +` + +export const StyledMenu = styled.div` + margin: 0; + display: flex; + justify-content: center; + align-items: center; + position: relative; + border: none; + text-align: left; + color: inherit; + + ${RowFixed} { + color: inherit; + + > div { + color: inherit; + opacity: 0.85; + } + } +` + +export const MenuFlyout = styled(MenuList)` + min-width: 20.125rem; + background: var(${UI.COLOR_PRIMARY}); + box-shadow: + 0px 0px 1px rgba(0, 0, 0, 0.01), + 0px 4px 8px rgba(0, 0, 0, 0.04), + 0px 16px 24px rgba(0, 0, 0, 0.04), + 0px 24px 32px rgba(0, 0, 0, 0.01); + border-radius: 12px; + display: flex; + flex-direction: column; + font-size: 1rem; + position: absolute; + z-index: 100; + color: inherit; + box-shadow: ${({ theme }) => theme.boxShadow2}; + border: 1px solid ${({ theme }) => transparentize(theme.white, 0.95)}; + background-color: var(${UI.COLOR_PAPER}); + color: inherit; + padding: 0; + margin: 0; + top: 36px; + right: 0; + width: 280px; + + ${Media.upToMedium()} { + min-width: 18.125rem; + } +` diff --git a/apps/cowswap-frontend/src/modules/swap/containers/TradeRateDetails/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx similarity index 72% rename from apps/cowswap-frontend/src/modules/swap/containers/TradeRateDetails/index.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx index 7c5aa2e111..6b18c7a060 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/TradeRateDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx @@ -1,41 +1,41 @@ -import React, { useMemo, useState, useCallback } from 'react' +import React, { useMemo, useState, useCallback, ReactElement } from 'react' -import { CurrencyAmount, Percent } from '@uniswap/sdk-core' +import { CurrencyAmount } from '@uniswap/sdk-core' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { getTotalCosts, - ReceiveAmountInfo, TradeFeesAndCosts, TradeTotalCostsDetails, useDerivedTradeState, NetworkCostsRow, + useReceiveAmountInfo, + useShouldPayGas, } from 'modules/trade' import { useTradeQuote } from 'modules/tradeQuote' +import { useIsSlippageModified, useTradeSlippage } from 'modules/tradeSlippage' import { useUsdAmount } from 'modules/usdAmount' import { NetworkCostsSuffix } from 'common/pure/NetworkCostsSuffix' import { RateInfoParams } from 'common/pure/RateInfo' -import { useShouldPayGas } from '../../hooks/useShouldPayGas' import { NetworkCostsTooltipSuffix } from '../../pure/NetworkCostsTooltipSuffix' -import { RowDeadline } from '../Row/RowDeadline' -import { RowSlippage } from '../Row/RowSlippage' +import { RowDeadline } from '../RowDeadline' +import { RowSlippage } from '../RowSlippage' interface TradeRateDetailsProps { - receiveAmountInfo: ReceiveAmountInfo | null + deadline: number rateInfoParams: RateInfoParams - allowedSlippage: Percent | null - isSlippageModified: boolean + children?: ReactElement + isTradePriceUpdating: boolean } -export function TradeRateDetails({ - allowedSlippage, - receiveAmountInfo, - rateInfoParams, - isSlippageModified, -}: TradeRateDetailsProps) { +export function TradeRateDetails({ rateInfoParams, deadline, isTradePriceUpdating }: TradeRateDetailsProps) { const [isFeeDetailsOpen, setFeeDetailsOpen] = useState(false) + + const slippage = useTradeSlippage() + const isSlippageModified = useIsSlippageModified() + const receiveAmountInfo = useReceiveAmountInfo() const derivedTradeState = useDerivedTradeState() const tradeQuote = useTradeQuote() const shouldPayGas = useShouldPayGas() @@ -90,8 +90,14 @@ export function TradeRateDetails({ networkCostsTooltipSuffix={} alwaysRow /> - {allowedSlippage && } - + {slippage && ( + + )} + ) } diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx similarity index 71% rename from apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx index 7ad9ea8175..274c563b77 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx @@ -14,31 +14,40 @@ import { } from '@cowprotocol/common-const' import { useOnClickOutside } from '@cowprotocol/common-hooks' import { getWrappedToken, percentToBps } from '@cowprotocol/common-utils' -import { FancyButton, HelpTooltip, Media, RowBetween, RowFixed, UI } from '@cowprotocol/ui' +import { StatefulValue } from '@cowprotocol/types' +import { HelpTooltip, 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' -import { darken } from 'color2k' -import styled, { ThemeContext } from 'styled-components/macro' +import { ThemeContext } from 'styled-components/macro' import { ThemedText } from 'theme' 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' -import { useSetSlippage } from 'modules/swap/hooks/useSetSlippage' -import { useDefaultSwapSlippage, useSmartSwapSlippage, useSwapSlippage } from 'modules/swap/hooks/useSwapSlippage' -import { getNativeOrderDeadlineTooltip, getNonNativeOrderDeadlineTooltip } from 'modules/swap/pure/Row/RowDeadline' -import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'modules/swap/pure/Row/RowSlippageContent' +import { useIsEoaEthFlow } from 'modules/trade' +import { + useDefaultTradeSlippage, + useIsSlippageModified, + useIsSmartSlippageApplied, + useSetSlippage, + useSmartTradeSlippage, + useTradeSlippage, +} from 'modules/tradeSlippage' +import { + getNativeOrderDeadlineTooltip, + getNativeSlippageTooltip, + getNonNativeOrderDeadlineTooltip, + getNonNativeSlippageTooltip, +} from 'common/utils/tradeSettingsTooltips' import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import * as styledEl from './styled' + const MAX_DEADLINE_MINUTES = 180 // 3h enum SlippageError { @@ -49,150 +58,26 @@ enum DeadlineError { InvalidInput = 'InvalidInput', } -const Option = styled(FancyButton)<{ active: boolean }>` - margin-right: 8px; - - :hover { - cursor: pointer; - } - - &:disabled { - border: none; - pointer-events: none; - } -` - -export const Input = styled.input` - background: var(${UI.COLOR_PAPER}); - font-size: 16px; - width: auto; - outline: none; - - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - -webkit-appearance: none; - } - - color: ${({ theme, color }) => (color === 'red' ? theme.error : `var(${UI.COLOR_TEXT})`)}; - text-align: right; -` - -export const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>` - height: 2rem; - position: relative; - padding: 0 0.75rem; - flex: 1; - border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.error : theme.bg2}`}; - - :hover { - border: ${({ theme, active, warning }) => - active && `1px solid ${warning ? darken(theme.error, 0.1) : darken(theme.bg2, 0.1)}`}; - } - - input { - width: 100%; - height: 100%; - border: 0; - border-radius: 2rem; - } -` - -const SlippageEmojiContainer = styled.span` - color: #f3841e; - - ${Media.upToSmall()} { - display: none; - } -` - -const SmartSlippageInfo = styled.div` - color: var(${UI.COLOR_GREEN}); - font-size: 13px; - text-align: right; - width: 100%; - padding-right: 0.2rem; - display: flex; - justify-content: flex-end; - padding-bottom: 0.35rem; - - > span { - margin-left: 4px; - } -` - -const Wrapper = styled.div` - ${RowBetween} > button, ${OptionCustom} { - &:disabled { - color: var(${UI.COLOR_TEXT_OPACITY_50}); - background-color: var(${UI.COLOR_PAPER}); - border: none; - pointer-events: none; - } - } - - ${OptionCustom} { - background-color: var(${UI.COLOR_PAPER_DARKER}); - border: 0; - color: inherit; - - > div > input { - background: transparent; - color: inherit; - - &:disabled { - color: inherit; - background-color: inherit; - } - } - - > div > input::placeholder { - opacity: 0.5; - color: inherit; - } - } - - ${RowFixed} { - color: inherit; - - > div { - color: inherit; - opacity: 0.85; - } - - > button { - background-color: var(${UI.COLOR_PAPER_DARKER}); - border: 0; - } - - > button > input { - background: transparent; - color: inherit; - } - - > button > input::placeholder { - background: transparent; - opacity: 0.5; - color: inherit; - } - } -` +interface TransactionSettingsProps { + deadlineState: StatefulValue +} -export function TransactionSettings() { +export function TransactionSettings({ deadlineState }: TransactionSettingsProps) { const { chainId } = useWalletInfo() const theme = useContext(ThemeContext) const isEoaEthFlow = useIsEoaEthFlow() const nativeCurrency = useNativeCurrency() - const swapSlippage = useSwapSlippage() - const defaultSwapSlippage = useDefaultSwapSlippage() + const swapSlippage = useTradeSlippage() + const defaultSwapSlippage = useDefaultTradeSlippage() const setSwapSlippage = useSetSlippage() const isSmartSlippageApplied = useIsSmartSlippageApplied() - const smartSlippage = useSmartSwapSlippage() + const smartSlippage = useSmartTradeSlippage() const chosenSlippageMatchesSmartSlippage = smartSlippage && new Percent(smartSlippage, 10_000).equalTo(swapSlippage) - const [deadline, setDeadline] = useUserTransactionTTL() + const [deadline, setDeadline] = deadlineState const widgetDeadline = useInjectedWidgetDeadline(TradeType.SWAP) const [slippageInput, setSlippageInput] = useState('') @@ -323,7 +208,7 @@ export function TransactionSettings() { useOnClickOutside([wrapperRef], onSlippageInputBlur) return ( - + @@ -340,24 +225,24 @@ export function TransactionSettings() { /> - - + + {!isSmartSlippageApplied && !chosenSlippageMatchesSmartSlippage && (tooLow || tooHigh) ? ( - + ⚠️ - + ) : null} - 0 ? slippageInput : !isSlippageModified ? '' : swapSlippage.toFixed(2)} onChange={(e) => parseSlippageInput(e.target.value)} @@ -366,15 +251,14 @@ export function TransactionSettings() { /> % - + {!isSmartSlippageApplied && !chosenSlippageMatchesSmartSlippage && (slippageError || tooLow || tooHigh) ? ( {slippageError ? ( @@ -391,7 +275,7 @@ export function TransactionSettings() { ) : null} {isSmartSlippageApplied && ( - + @@ -401,7 +285,7 @@ export function TransactionSettings() { } /> Dynamic - + )} @@ -423,8 +307,8 @@ export function TransactionSettings() { /> - - + 0 @@ -441,7 +325,7 @@ export function TransactionSettings() { color={deadlineError ? 'red' : ''} disabled={isDeadlineDisabled} /> - + minutes @@ -449,6 +333,6 @@ export function TransactionSettings() { )} - + ) } diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/styled.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/styled.tsx new file mode 100644 index 0000000000..d3ef4a7381 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/styled.tsx @@ -0,0 +1,131 @@ +import { FancyButton, Media, RowBetween, RowFixed, UI } from '@cowprotocol/ui' + +import { darken } from 'color2k' +import styled from 'styled-components/macro' + +export const Option = styled(FancyButton)<{ active: boolean }>` + margin-right: 8px; + + :hover { + cursor: pointer; + } + + &:disabled { + border: none; + pointer-events: none; + } +` + +export const Input = styled.input` + background: var(${UI.COLOR_PAPER}); + font-size: 16px; + width: auto; + outline: none; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + } + + color: ${({ theme, color }) => (color === 'red' ? theme.error : `var(${UI.COLOR_TEXT})`)}; + text-align: right; +` + +export const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>` + height: 2rem; + position: relative; + padding: 0 0.75rem; + flex: 1; + border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.error : theme.bg2}`}; + + :hover { + border: ${({ theme, active, warning }) => + active && `1px solid ${warning ? darken(theme.error, 0.1) : darken(theme.bg2, 0.1)}`}; + } + + input { + width: 100%; + height: 100%; + border: 0; + border-radius: 2rem; + } +` + +export const SlippageEmojiContainer = styled.span` + color: #f3841e; + ${Media.upToSmall()} { + display: none; + } +` + +export const SmartSlippageInfo = styled.div` + color: var(${UI.COLOR_GREEN}); + font-size: 13px; + text-align: right; + width: 100%; + padding-right: 0.2rem; + display: flex; + justify-content: flex-end; + padding-bottom: 0.35rem; + + > span { + margin-left: 4px; + } +` + +export const Wrapper = styled.div` + ${RowBetween} > button, ${OptionCustom} { + &:disabled { + color: var(${UI.COLOR_TEXT_OPACITY_50}); + background-color: var(${UI.COLOR_PAPER}); + border: none; + pointer-events: none; + } + } + + ${OptionCustom} { + background-color: var(${UI.COLOR_PAPER_DARKER}); + border: 0; + color: inherit; + + > div > input { + background: transparent; + color: inherit; + + &:disabled { + color: inherit; + background-color: inherit; + } + } + + > div > input::placeholder { + opacity: 0.5; + color: inherit; + } + } + + ${RowFixed} { + color: inherit; + + > div { + color: inherit; + opacity: 0.85; + } + + > button { + background-color: var(${UI.COLOR_PAPER_DARKER}); + border: 0; + } + + > button > input { + background: transparent; + color: inherit; + } + + > button > input::placeholder { + background: transparent; + opacity: 0.5; + color: inherit; + } + } +` diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/index.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/index.ts new file mode 100644 index 0000000000..5506835feb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/index.ts @@ -0,0 +1,7 @@ +export { RowDeadline } from './containers/RowDeadline' +export { TradeRateDetails } from './containers/TradeRateDetails' +export { SettingsTab } from './containers/SettingsTab' +export { HighFeeWarning } from './containers/HighFeeWarning' +export { BundleTxWrapBanner } from './containers/BundleTxWrapBanner' +export { useHighFeeWarning } from './containers/HighFeeWarning/hooks/useHighFeeWarning' +export { NetworkCostsTooltipSuffix } from './pure/NetworkCostsTooltipSuffix' diff --git a/apps/cowswap-frontend/src/modules/swap/pure/NetworkCostsTooltipSuffix.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/NetworkCostsTooltipSuffix.tsx similarity index 93% rename from apps/cowswap-frontend/src/modules/swap/pure/NetworkCostsTooltipSuffix.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/NetworkCostsTooltipSuffix.tsx index 60c9cc8290..204feb6e6a 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/NetworkCostsTooltipSuffix.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/NetworkCostsTooltipSuffix.tsx @@ -1,9 +1,9 @@ import { isTruthy } from '@cowprotocol/common-utils' import { useWalletDetails } from '@cowprotocol/wallet' -import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import { useIsEoaEthFlow } from 'modules/trade' -import { useIsEoaEthFlow } from '../hooks/useIsEoaEthFlow' +import useNativeCurrency from 'lib/hooks/useNativeCurrency' export function NetworkCostsTooltipSuffix() { const { allowsOffchainSigning } = useWalletDetails() diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.cosmos.tsx similarity index 92% rename from apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.cosmos.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.cosmos.tsx index 06633ec398..7b353c6f7f 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.cosmos.tsx @@ -3,7 +3,6 @@ import { MINIMUM_ETH_FLOW_DEADLINE_SECONDS } from '@cowprotocol/common-const' import { RowDeadlineContent, RowDeadlineProps } from '.' const defaultProps: RowDeadlineProps = { - toggleSettings: console.log, isEoaEthFlow: true, displayDeadline: Math.floor(MINIMUM_ETH_FLOW_DEADLINE_SECONDS / 60) + ' minutes', symbols: ['ETH', 'WETH'], diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.tsx new file mode 100644 index 0000000000..2cc56caa79 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.tsx @@ -0,0 +1,51 @@ +import { HoverTooltip, RowFixed } from '@cowprotocol/ui' + +import { Trans } from '@lingui/macro' + +import { getNativeOrderDeadlineTooltip, getNonNativeOrderDeadlineTooltip } from 'common/utils/tradeSettingsTooltips' + +import { StyledRowBetween, TextWrapper, StyledInfoIcon, TransactionText, RowStyleProps } from '../styled' + +export interface RowDeadlineProps { + isEoaEthFlow: boolean + symbols?: (string | undefined)[] + displayDeadline: string + styleProps?: RowStyleProps + userDeadline: number + slippageLabel?: React.ReactNode + slippageTooltip?: React.ReactNode +} + +export function RowDeadlineContent(props: RowDeadlineProps) { + const { displayDeadline, isEoaEthFlow, symbols, styleProps } = props + const deadlineTooltipContent = isEoaEthFlow + ? getNativeOrderDeadlineTooltip(symbols) + : getNonNativeOrderDeadlineTooltip() + + return ( + + + + + + + + + + + {displayDeadline} + + + ) +} + +type DeadlineTextContentsProps = { isEoaEthFlow: boolean } + +function DeadlineTextContents({ isEoaEthFlow }: DeadlineTextContentsProps) { + return ( + + Transaction expiration + {isEoaEthFlow && (modified)} + + ) +} diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.cosmos.tsx similarity index 74% rename from apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.cosmos.tsx index f180f5effb..84cd07a457 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.cosmos.tsx @@ -1,6 +1,6 @@ import { Percent } from '@uniswap/sdk-core' -import { RowSlippageContent, RowSlippageContentProps } from 'modules/swap/pure/Row/RowSlippageContent' +import { RowSlippageContent, RowSlippageContentProps } from './index' const defaultProps: RowSlippageContentProps = { chainId: 1, @@ -10,9 +10,6 @@ const defaultProps: RowSlippageContentProps = { get displaySlippage() { return this.isEoaEthFlow ? '2%' : '0.2%' }, - toggleSettings() { - console.log('RowSlippageContent settings toggled!') - }, isSlippageModified: false, isSmartSlippageApplied: false, smartSlippage: '0.2%', diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx similarity index 55% rename from apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx index 07a343a2fd..75e4388c4b 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx @@ -1,4 +1,5 @@ -import { MINIMUM_ETH_FLOW_SLIPPAGE, PERCENTAGE_PRECISION } from '@cowprotocol/common-const' +import { useSetAtom } from 'jotai' + import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' import { CenteredDots, HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' @@ -7,24 +8,10 @@ import { Percent } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' import styled from 'styled-components/macro' -import { StyledRowBetween, TextWrapper } from 'modules/swap/pure/Row/styled' -import { RowStyleProps } from 'modules/swap/pure/Row/types' -import { StyledInfoIcon, TransactionText } from 'modules/swap/pure/styled' - -export const ClickableText = styled.button` - background: none; - border: none; - outline: none; - padding: 0; - margin: 0; - font-size: inherit; - font-weight: inherit; - color: inherit; - - > div { - display: inline-block; - } -` +import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' + +import { settingsTabStateAtom } from '../../../state/settingsTabState' +import { StyledRowBetween, TextWrapper, StyledInfoIcon, TransactionText, RowStyleProps } from '../styled' const DefaultSlippage = styled.span` display: inline-flex; @@ -42,47 +29,17 @@ const DefaultSlippage = styled.span` } ` -export const getNativeSlippageTooltip = (chainId: SupportedChainId, symbols: (string | undefined)[] | undefined) => ( - - When selling {symbols?.[0] || 'a native currency'}, the minimum slippage tolerance is set to{' '} - {MINIMUM_ETH_FLOW_SLIPPAGE[chainId].toSignificant(PERCENTAGE_PRECISION)}% to ensure a high likelihood of order - matching, even in volatile market conditions. -
-
- {symbols?.[0] || 'Native currency'} orders can, in rare cases, be frontrun due to their on-chain component. For more - robust MEV protection, consider wrapping your {symbols?.[0] || 'native currency'} before trading. -
-) -export const getNonNativeSlippageTooltip = (isSettingsModal?: boolean) => ( - - CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting the - best price.{' '} - {isSettingsModal ? ( - <> - To override this, enter your desired slippage amount. -
-
- Either way, your slippage is protected from MEV! - - ) : ( - "Trades are protected from MEV, so your slippage can't be exploited!" - )} -
-) - const SUGGESTED_SLIPPAGE_TOOLTIP = 'This is the recommended slippage tolerance based on current gas prices & volatility. A lower amount may result in slower execution.' export interface RowSlippageContentProps { chainId: SupportedChainId - toggleSettings: Command displaySlippage: string isEoaEthFlow: boolean symbols?: (string | undefined)[] wrappedSymbol?: string styleProps?: RowStyleProps allowedSlippage: Percent - showSettingOnClick?: boolean slippageLabel?: React.ReactNode slippageTooltip?: React.ReactNode isSlippageModified: boolean @@ -92,13 +49,9 @@ export interface RowSlippageContentProps { isSmartSlippageLoading: boolean } -// TODO: RowDeadlineContent and RowSlippageContent are very similar. Refactor and extract base component? - export function RowSlippageContent(props: RowSlippageContentProps) { const { chainId, - showSettingOnClick, - toggleSettings, displaySlippage, isEoaEthFlow, symbols, @@ -112,6 +65,10 @@ export function RowSlippageContent(props: RowSlippageContentProps) { isSmartSlippageLoading, } = props + const setSettingTabState = useSetAtom(settingsTabStateAtom) + + const openSettings = () => setSettingTabState({ open: true }) + const tooltipContent = slippageTooltip || (isEoaEthFlow ? getNativeSlippageTooltip(chainId, symbols) : getNonNativeSlippageTooltip()) @@ -149,33 +106,19 @@ export function RowSlippageContent(props: RowSlippageContentProps) { return ( - - {showSettingOnClick ? ( - - - - ) : ( - - )} + + - - {showSettingOnClick ? ( - {displaySlippageWithLoader} - ) : ( - {displaySlippageWithLoader} - )} + + {displaySlippageWithLoader} ) diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/styled.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/styled.ts similarity index 73% rename from apps/cowswap-frontend/src/modules/swap/pure/Row/styled.ts rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/styled.ts index 0ace5489f6..121d164342 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/styled.ts +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/styled.ts @@ -2,10 +2,15 @@ import { Media, UI } from '@cowprotocol/ui' import { RowBetween, RowFixed } from '@cowprotocol/ui' import { HoverTooltip } from '@cowprotocol/ui' +import { Info } from 'react-feather' import { Text } from 'rebass' import styled from 'styled-components/macro' -import { RowStyleProps } from './types' +export interface RowStyleProps { + fontWeight?: number + fontSize?: number + alignContentRight?: boolean +} const StyledHoverTooltip = styled(HoverTooltip)`` export const TextWrapper = styled(Text)<{ success?: boolean }>` @@ -58,3 +63,25 @@ export const StyledRowBetween = styled(RowBetween)` color: inherit; } ` + +export const StyledInfoIcon = styled(Info)` + color: inherit; + opacity: 0.6; + line-height: 0; + vertical-align: middle; + transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; + + &:hover { + opacity: 1; + } +` + +export const TransactionText = styled.span` + display: flex; + gap: 3px; + cursor: pointer; + + > i { + font-style: normal; + } +` diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts new file mode 100644 index 0000000000..659fb6321f --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai' + +export const settingsTabStateAtom = atom({ open: false }) diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/index.tsx index 9ea59d9265..ad08864297 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/index.tsx @@ -1,13 +1,11 @@ import { useAtomValue } from 'jotai' -import React, { useState } from 'react' +import React from 'react' import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders' -import { useInjectedWidgetParams } from 'modules/injectedWidget' import { TradeConfirmation, TradeConfirmModal, useTradeConfirmActions, useTradePriceImpact } from 'modules/trade' import { TradeBasicConfirmDetails } from 'modules/trade/containers/TradeBasicConfirmDetails' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { DividerHorizontal } from 'modules/trade/pure/Row/styled' import { PRICE_UPDATE_INTERVAL } from 'modules/tradeQuote/hooks/useTradeQuotePolling' @@ -20,7 +18,6 @@ import { useCreateTwapOrder } from '../../hooks/useCreateTwapOrder' import { useIsFallbackHandlerRequired } from '../../hooks/useFallbackHandlerVerification' import { useTwapFormState } from '../../hooks/useTwapFormState' import { useTwapSlippage } from '../../hooks/useTwapSlippage' -import { useTwapWarningsContext } from '../../hooks/useTwapWarningsContext' import { scaledReceiveAmountInfoAtom } from '../../state/scaledReceiveAmountInfoAtom' import { twapOrderAtom } from '../../state/twapOrderAtom' import { TwapFormWarnings } from '../TwapFormWarnings' @@ -70,15 +67,10 @@ export function TwapConfirmModal() { const twapOrder = useAtomValue(twapOrderAtom) const receiveAmountInfo = useAtomValue(scaledReceiveAmountInfoAtom) const slippage = useTwapSlippage() - const { showPriceImpactWarning } = useTwapWarningsContext() const localFormValidation = useTwapFormState() const tradeConfirmActions = useTradeConfirmActions() const createTwapOrder = useCreateTwapOrder() - const widgetParams = useInjectedWidgetParams() - - const isInvertedState = useState(false) - const isConfirmDisabled = !!localFormValidation const priceImpact = useTradePriceImpact() @@ -121,40 +113,40 @@ export function TwapConfirmModal() { refreshInterval={PRICE_UPDATE_INTERVAL} recipient={recipient} > - <> - {receiveAmountInfo && numOfParts && ( - : null, - networkCostsTooltipSuffix: !allowsOffchainSigning ? ( - <> -
-
- Because you are using a smart contract wallet, you will pay a separate gas cost for signing the - order placement on-chain. - - ) : null, - }} + {(warnings) => ( + <> + {receiveAmountInfo && numOfParts && ( + : null, + networkCostsTooltipSuffix: !allowsOffchainSigning ? ( + <> +
+
+ Because you are using a smart contract wallet, you will pay a separate gas cost for signing the + order placement on-chain. + + ) : null, + }} + /> + )} + + - )} - - - {showPriceImpactWarning && } - - + {warnings} + + + )} ) diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx index 07fd7f1b14..ec1f665172 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx @@ -1,20 +1,14 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useCallback } from 'react' -import { BundleTxApprovalBanner } from '@cowprotocol/ui' import { useIsSafeViaWc, useWalletInfo } from '@cowprotocol/wallet' -import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders' import { modifySafeHandlerAnalytics } from 'modules/analytics' import { SellNativeWarningBanner } from 'modules/trade/containers/SellNativeWarningBanner' import { useTradeRouteContext } from 'modules/trade/hooks/useTradeRouteContext' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { useGetTradeFormValidation } from 'modules/tradeFormValidation' import { TradeFormValidation } from 'modules/tradeFormValidation/types' import { useTradeQuoteFeeFiatAmount } from 'modules/tradeQuote' -import { useShouldZeroApprove } from 'modules/zeroApproval' - -import { ZeroApprovalWarning } from 'common/pure/ZeroApprovalWarning' import { FallbackHandlerWarning, @@ -31,32 +25,28 @@ import { useTwapSlippage } from '../../hooks/useTwapSlippage' import { useTwapWarningsContext } from '../../hooks/useTwapWarningsContext' import { TwapFormState } from '../../pure/PrimaryActionButton/getTwapFormState' import { swapAmountDifferenceAtom } from '../../state/swapAmountDifferenceAtom' -import { twapDeadlineAtom, twapOrderAtom } from '../../state/twapOrderAtom' +import { twapDeadlineAtom } from '../../state/twapOrderAtom' import { twapOrdersSettingsAtom, updateTwapOrdersSettingsAtom } from '../../state/twapOrdersSettingsAtom' import { isPriceProtectionNotEnough } from '../../utils/isPriceProtectionNotEnough' -const BUNDLE_APPROVAL_STATES = [TradeFormValidation.ApproveAndSwap] - interface TwapFormWarningsProps { localFormValidation: TwapFormState | null isConfirmationModal?: boolean } export function TwapFormWarnings({ localFormValidation, isConfirmationModal }: TwapFormWarningsProps) { - const { isFallbackHandlerSetupAccepted, isPriceImpactAccepted } = useAtomValue(twapOrdersSettingsAtom) + const { isFallbackHandlerSetupAccepted } = useAtomValue(twapOrdersSettingsAtom) const updateTwapOrdersSettings = useSetAtom(updateTwapOrdersSettingsAtom) - const twapOrder = useAtomValue(twapOrderAtom) const slippage = useTwapSlippage() const deadline = useAtomValue(twapDeadlineAtom) const swapAmountDifference = useAtomValue(swapAmountDifferenceAtom) - const { outputCurrencyAmount } = useAdvancedOrdersDerivedState() const primaryFormValidation = useGetTradeFormValidation() const { chainId } = useWalletInfo() const isFallbackHandlerRequired = useIsFallbackHandlerRequired() const isSafeViaWc = useIsSafeViaWc() const tradeQuoteFeeFiatAmount = useTradeQuoteFeeFiatAmount() - const { canTrade, showPriceImpactWarning, walletIsNotConnected } = useTwapWarningsContext() + const { canTrade, walletIsNotConnected } = useTwapWarningsContext() const tradeUrlParams = useTradeRouteContext() const toggleFallbackHandlerSetupFlag = useCallback( @@ -67,17 +57,9 @@ export function TwapFormWarnings({ localFormValidation, isConfirmationModal }: T [updateTwapOrdersSettings], ) - const shouldZeroApprove = useShouldZeroApprove(twapOrder?.sellAmount) - const showZeroApprovalWarning = !isConfirmationModal && shouldZeroApprove && outputCurrencyAmount !== null - const showApprovalBundlingBanner = - !isConfirmationModal && primaryFormValidation && BUNDLE_APPROVAL_STATES.includes(primaryFormValidation) const showTradeFormWarnings = !isConfirmationModal && canTrade const showFallbackHandlerWarning = showTradeFormWarnings && isFallbackHandlerRequired - const setIsPriceImpactAccepted = useCallback(() => { - updateTwapOrdersSettings({ isPriceImpactAccepted: !isPriceImpactAccepted }) - }, [updateTwapOrdersSettings, isPriceImpactAccepted]) - // Don't display any warnings while a wallet is not connected if (walletIsNotConnected) return null @@ -91,17 +73,6 @@ export function TwapFormWarnings({ localFormValidation, isConfirmationModal }: T return ( <> - {showZeroApprovalWarning && } - {showApprovalBundlingBanner && } - - {!isConfirmationModal && showPriceImpactWarning && ( - setIsPriceImpactAccepted()} - /> - )} - {(() => { if (localFormValidation === TwapFormState.NOT_SAFE) { return 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 41050dad54..3050f4d664 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx @@ -1,5 +1,5 @@ import { useAtomValue, useSetAtom } from 'jotai' -import { useEffect, useLayoutEffect, useMemo, useState } from 'react' +import { ReactNode, useEffect, useLayoutEffect, useMemo, useState } from 'react' import { renderTooltip } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' @@ -48,7 +48,11 @@ import { TwapFormWarnings } from '../TwapFormWarnings' export type { LabelTooltip, LabelTooltipItems } from './tooltips' -export function TwapFormWidget() { +interface TwapFormWidget { + tradeWarnings: ReactNode +} + +export function TwapFormWidget({ tradeWarnings }: TwapFormWidget) { const { account } = useWalletInfo() const { numberOfPartsValue, deadline, customDeadline, isCustomDeadline } = useAtomValue(twapOrdersSettingsAtom) @@ -110,7 +114,7 @@ export function TwapFormWidget() { // Reset warnings flags once on start useEffect(() => { - updateSettingsState({ isFallbackHandlerSetupAccepted: false, isPriceImpactAccepted: false }) + updateSettingsState({ isFallbackHandlerSetupAccepted: false }) openAdvancedOrdersTabAnalytics() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -217,6 +221,7 @@ export function TwapFormWidget() { + {tradeWarnings} { const canTrade = !primaryFormValidation || NOT_BLOCKING_VALIDATIONS.includes(primaryFormValidation) - const showPriceImpactWarning = canTrade && !priceImpactParams.loading && !priceImpactParams.priceImpact const walletIsNotConnected = !account return { canTrade, - showPriceImpactWarning, walletIsNotConnected, } - }, [primaryFormValidation, account, priceImpactParams]) + }, [primaryFormValidation, account]) } diff --git a/apps/cowswap-frontend/src/modules/twap/state/twapOrdersSettingsAtom.ts b/apps/cowswap-frontend/src/modules/twap/state/twapOrdersSettingsAtom.ts index faaecd4b8d..fa99ca5455 100644 --- a/apps/cowswap-frontend/src/modules/twap/state/twapOrdersSettingsAtom.ts +++ b/apps/cowswap-frontend/src/modules/twap/state/twapOrdersSettingsAtom.ts @@ -21,7 +21,6 @@ export interface TwapOrdersSettingsState extends TwapOrdersDeadline { readonly numberOfPartsValue: number readonly slippageValue: number | null readonly isFallbackHandlerSetupAccepted: boolean - readonly isPriceImpactAccepted: boolean } export const defaultCustomDeadline: TwapOrdersDeadline['customDeadline'] = { @@ -38,13 +37,12 @@ export const defaultTwapOrdersSettings: TwapOrdersSettingsState = { // null = auto slippageValue: null, isFallbackHandlerSetupAccepted: false, - isPriceImpactAccepted: false, } export const twapOrdersSettingsAtom = atomWithStorage( 'twap-orders-settings-atom:v1', defaultTwapOrdersSettings, - getJotaiIsolatedStorage() + getJotaiIsolatedStorage(), ) export const updateTwapOrdersSettingsAtom = atom(null, (get, set, nextState: Partial) => { diff --git a/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts b/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts index ebe42b5f42..60b30aa4e0 100644 --- a/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts +++ b/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts @@ -49,4 +49,5 @@ const TradeTypeMap: Record = { [TradeType.SWAP]: WidgetTradeType.SWAP, [TradeType.LIMIT_ORDER]: WidgetTradeType.LIMIT, [TradeType.ADVANCED_ORDERS]: WidgetTradeType.ADVANCED, + [TradeType.YIELD]: WidgetTradeType.YIELD, } diff --git a/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx new file mode 100644 index 0000000000..5dc96fefef --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx @@ -0,0 +1,33 @@ +import { useIsNoImpactWarningAccepted, useTradeConfirmActions } from 'modules/trade' +import { TradeFormButtons, useGetTradeFormValidation, useTradeFormButtonContext } from 'modules/tradeFormValidation' +import { useHighFeeWarning } from 'modules/tradeWidgetAddons' + +const CONFIRM_TEXT = 'Swap' + +interface TradeButtonsProps { + isTradeContextReady: boolean +} + +export function TradeButtons({ isTradeContextReady }: TradeButtonsProps) { + const primaryFormValidation = useGetTradeFormValidation() + const tradeConfirmActions = useTradeConfirmActions() + const { feeWarningAccepted } = useHighFeeWarning() + const isNoImpactWarningAccepted = useIsNoImpactWarningAccepted() + + const confirmTrade = tradeConfirmActions.onOpen + + const tradeFormButtonContext = useTradeFormButtonContext(CONFIRM_TEXT, confirmTrade) + + const isDisabled = !isTradeContextReady || !feeWarningAccepted || !isNoImpactWarningAccepted + + if (!tradeFormButtonContext) return null + + return ( + + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/containers/Warnings/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/Warnings/index.tsx new file mode 100644 index 0000000000..c81c3644e0 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/Warnings/index.tsx @@ -0,0 +1,9 @@ +import { HighFeeWarning } from 'modules/tradeWidgetAddons' + +export function Warnings() { + return ( + <> + + + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldConfirmModal/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldConfirmModal/index.tsx new file mode 100644 index 0000000000..08b205bac0 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldConfirmModal/index.tsx @@ -0,0 +1,100 @@ +import React, { useMemo } from 'react' + +import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' + +import type { PriceImpact } from 'legacy/hooks/usePriceImpact' + +import { useAppData } from 'modules/appData' +import { + TradeBasicConfirmDetails, + TradeConfirmation, + TradeConfirmModal, + useOrderSubmittedContent, + useReceiveAmountInfo, + useTradeConfirmActions, +} from 'modules/trade' +import { HighFeeWarning } from 'modules/tradeWidgetAddons' + +import { useRateInfoParams } from 'common/hooks/useRateInfoParams' +import { CurrencyPreviewInfo } from 'common/pure/CurrencyAmountPreview' +import { getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' + +import { useYieldDerivedState } from '../../hooks/useYieldDerivedState' + +const CONFIRM_TITLE = 'Confirm order' + +const labelsAndTooltips = { + slippageTooltip: getNonNativeSlippageTooltip(), +} + +export interface YieldConfirmModalProps { + doTrade(): Promise + + inputCurrencyInfo: CurrencyPreviewInfo + outputCurrencyInfo: CurrencyPreviewInfo + priceImpact: PriceImpact + recipient?: string | null +} + +export function YieldConfirmModal(props: YieldConfirmModalProps) { + const { inputCurrencyInfo, outputCurrencyInfo, priceImpact, recipient, doTrade: _doTrade } = props + + /** + * This is a very important part of the code. + * After the confirmation modal opens, the trade context should not be recreated. + * In order to prevent this, we use useMemo to keep the trade context the same when the modal was opened. + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + const doTrade = useMemo(() => _doTrade, []) + + const { account, chainId } = useWalletInfo() + const { ensName } = useWalletDetails() + const appData = useAppData() + const receiveAmountInfo = useReceiveAmountInfo() + const tradeConfirmActions = useTradeConfirmActions() + const { slippage } = useYieldDerivedState() + + const rateInfoParams = useRateInfoParams(inputCurrencyInfo.amount, outputCurrencyInfo.amount) + const submittedContent = useOrderSubmittedContent(chainId) + + return ( + + + {(restContent) => ( + <> + {receiveAmountInfo && slippage && ( + + )} + {restContent} + + + )} + + + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx new file mode 100644 index 0000000000..b034093df9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx @@ -0,0 +1,136 @@ +import { ReactNode, useCallback } from 'react' + +import { Field } from 'legacy/state/types' + +import { + TradeWidget, + TradeWidgetSlots, + useReceiveAmountInfo, + useTradeConfirmState, + useTradePriceImpact, +} from 'modules/trade' +import { useHandleSwap } from 'modules/tradeFlow' +import { useTradeQuote } from 'modules/tradeQuote' +import { SettingsTab, TradeRateDetails } from 'modules/tradeWidgetAddons' + +import { useRateInfoParams } from 'common/hooks/useRateInfoParams' +import { useSafeMemoObject } from 'common/hooks/useSafeMemo' +import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' + +import { useYieldDerivedState } from '../../hooks/useYieldDerivedState' +import { useYieldDeadlineState, useYieldRecipientToggleState, useYieldSettings } from '../../hooks/useYieldSettings' +import { useYieldWidgetActions } from '../../hooks/useYieldWidgetActions' +import { TradeButtons } from '../TradeButtons' +import { Warnings } from '../Warnings' +import { YieldConfirmModal } from '../YieldConfirmModal' + +export function YieldWidget() { + const { showRecipient } = useYieldSettings() + const deadlineState = useYieldDeadlineState() + const recipientToggleState = useYieldRecipientToggleState() + const { isLoading: isRateLoading } = useTradeQuote() + const priceImpact = useTradePriceImpact() + const { isOpen: isConfirmOpen } = useTradeConfirmState() + const widgetActions = useYieldWidgetActions() + const receiveAmountInfo = useReceiveAmountInfo() + + const { + inputCurrency, + outputCurrency, + inputCurrencyAmount, + outputCurrencyAmount, + inputCurrencyBalance, + outputCurrencyBalance, + inputCurrencyFiatAmount, + outputCurrencyFiatAmount, + recipient, + } = useYieldDerivedState() + const doTrade = useHandleSwap(useSafeMemoObject({ deadline: deadlineState[0] })) + + const inputCurrencyInfo: CurrencyInfo = { + field: Field.INPUT, + currency: inputCurrency, + amount: inputCurrencyAmount, + isIndependent: true, + balance: inputCurrencyBalance, + fiatAmount: inputCurrencyFiatAmount, + receiveAmountInfo: null, + } + const outputCurrencyInfo: CurrencyInfo = { + field: Field.OUTPUT, + currency: outputCurrency, + amount: outputCurrencyAmount, + isIndependent: false, + balance: outputCurrencyBalance, + fiatAmount: outputCurrencyFiatAmount, + receiveAmountInfo, + } + const inputCurrencyPreviewInfo = { + amount: inputCurrencyInfo.amount, + fiatAmount: inputCurrencyInfo.fiatAmount, + balance: inputCurrencyInfo.balance, + label: 'Sell amount', + } + + const outputCurrencyPreviewInfo = { + amount: outputCurrencyInfo.amount, + fiatAmount: outputCurrencyInfo.fiatAmount, + balance: outputCurrencyInfo.balance, + label: 'Receive (before fees)', + } + + const rateInfoParams = useRateInfoParams(inputCurrencyInfo.amount, outputCurrencyInfo.amount) + + const slots: TradeWidgetSlots = { + settingsWidget: , + bottomContent: useCallback( + (tradeWarnings: ReactNode | null) => { + return ( + <> + + + {tradeWarnings} + + + ) + }, + [doTrade.contextIsReady, isRateLoading, rateInfoParams, deadlineState], + ), + } + + const params = { + compactView: true, + enableSmartSlippage: true, + recipient, + showRecipient, + isTradePriceUpdating: isRateLoading, + priceImpact, + disableQuotePolling: isConfirmOpen, + } + + return ( + + ) : null + } + /> + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateCurrencyAmount.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateCurrencyAmount.ts new file mode 100644 index 0000000000..701a2d52ae --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateCurrencyAmount.ts @@ -0,0 +1,22 @@ +import { useCallback } from 'react' + +import { FractionUtils } from '@cowprotocol/common-utils' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import { Field } from 'legacy/state/types' + +import { useUpdateYieldRawState } from './useUpdateYieldRawState' + +export function useUpdateCurrencyAmount() { + const updateYieldState = useUpdateYieldRawState() + + return useCallback( + (field: Field, value: CurrencyAmount) => { + updateYieldState({ + [field === Field.INPUT ? 'inputCurrencyAmount' : 'outputCurrencyAmount']: + FractionUtils.serializeFractionToJSON(value), + }) + }, + [updateYieldState], + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateYieldRawState.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateYieldRawState.ts new file mode 100644 index 0000000000..11b110957d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateYieldRawState.ts @@ -0,0 +1,7 @@ +import { useSetAtom } from 'jotai' + +import { updateYieldRawStateAtom } from '../state/yieldRawStateAtom' + +export function useUpdateYieldRawState() { + return useSetAtom(updateYieldRawStateAtom) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldDerivedState.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldDerivedState.ts new file mode 100644 index 0000000000..d2554d390a --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldDerivedState.ts @@ -0,0 +1,26 @@ +import { useAtomValue } from 'jotai' +import { useSetAtom } from 'jotai/index' +import { useEffect } from 'react' + +import { INITIAL_ALLOWED_SLIPPAGE_PERCENT } from '@cowprotocol/common-const' + +import { TradeType, useBuildTradeDerivedState } from 'modules/trade' + +import { yieldDerivedStateAtom, yieldRawStateAtom } from '../state/yieldRawStateAtom' + +export function useYieldDerivedState() { + return useAtomValue(yieldDerivedStateAtom) +} + +export function useFillYieldDerivedState() { + const updateDerivedState = useSetAtom(yieldDerivedStateAtom) + const derivedState = useBuildTradeDerivedState(yieldRawStateAtom) + + useEffect(() => { + updateDerivedState({ + ...derivedState, + slippage: INITIAL_ALLOWED_SLIPPAGE_PERCENT, + tradeType: TradeType.YIELD, + }) + }, [derivedState, updateDerivedState]) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldRawState.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldRawState.ts new file mode 100644 index 0000000000..bc09f9631d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldRawState.ts @@ -0,0 +1,7 @@ +import { useAtomValue } from 'jotai' + +import { yieldRawStateAtom } from '../state/yieldRawStateAtom' + +export function useYieldRawState() { + return useAtomValue(yieldRawStateAtom) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts new file mode 100644 index 0000000000..6ba8f042a7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts @@ -0,0 +1,30 @@ +import { useSetAtom } from 'jotai' +import { useAtomValue } from 'jotai/index' +import { useMemo } from 'react' + +import { StatefulValue } from '@cowprotocol/types' + +import { updateYieldSettingsAtom, yieldSettingsAtom } from '../state/yieldSettingsAtom' + +export function useYieldSettings() { + return useAtomValue(yieldSettingsAtom) +} + +export function useYieldDeadlineState(): StatefulValue { + const updateState = useSetAtom(updateYieldSettingsAtom) + const settings = useYieldSettings() + + return useMemo( + () => [settings.deadline, (deadline: number) => updateState({ deadline })], + [settings.deadline, updateState], + ) +} +export function useYieldRecipientToggleState(): StatefulValue { + const updateState = useSetAtom(updateYieldSettingsAtom) + const settings = useYieldSettings() + + return useMemo( + () => [settings.showRecipient, (showRecipient: boolean) => updateState({ showRecipient })], + [settings.showRecipient, updateState], + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts new file mode 100644 index 0000000000..88b044dc26 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts @@ -0,0 +1,47 @@ +import { useCallback, useMemo } from 'react' + +import { isSellOrder, tryParseCurrencyAmount } from '@cowprotocol/common-utils' +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { Field } from 'legacy/state/types' + +import { TradeWidgetActions, useIsWrapOrUnwrap, useOnCurrencySelection, useSwitchTokensPlaces } from 'modules/trade' + +import { useUpdateCurrencyAmount } from './useUpdateCurrencyAmount' +import { useUpdateYieldRawState } from './useUpdateYieldRawState' +import { useYieldDerivedState } from './useYieldDerivedState' + +export function useYieldWidgetActions(): TradeWidgetActions { + const { inputCurrency, outputCurrency, orderKind } = useYieldDerivedState() + const isWrapOrUnwrap = useIsWrapOrUnwrap() + const updateYieldState = useUpdateYieldRawState() + const onCurrencySelection = useOnCurrencySelection() + const updateCurrencyAmount = useUpdateCurrencyAmount() + + const onUserInput = useCallback( + (field: Field, typedValue: string) => { + const currency = field === Field.INPUT ? inputCurrency : outputCurrency + + if (!currency) return + + const value = tryParseCurrencyAmount(typedValue, currency) || null + + updateCurrencyAmount(field, value) + }, + [updateCurrencyAmount, isWrapOrUnwrap, inputCurrency, outputCurrency], + ) + + const onSwitchTokens = useSwitchTokensPlaces({ + orderKind: isSellOrder(orderKind) ? OrderKind.BUY : OrderKind.SELL, + }) + + const onChangeRecipient = useCallback( + (recipient: string | null) => updateYieldState({ recipient }), + [updateYieldState], + ) + + return useMemo( + () => ({ onUserInput, onSwitchTokens, onChangeRecipient, onCurrencySelection }), + [onUserInput, onSwitchTokens, onChangeRecipient, onCurrencySelection], + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/index.ts b/apps/cowswap-frontend/src/modules/yield/index.ts new file mode 100644 index 0000000000..54d34b9a12 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/index.ts @@ -0,0 +1,5 @@ +export { YieldWidget } from './containers/YieldWidget' +export { useYieldRawState } from './hooks/useYieldRawState' +export { useUpdateYieldRawState } from './hooks/useUpdateYieldRawState' +export { YieldUpdaters } from './updaters' +export { yieldDerivedStateAtom } from './state/yieldRawStateAtom' diff --git a/apps/cowswap-frontend/src/modules/yield/state/yieldRawStateAtom.ts b/apps/cowswap-frontend/src/modules/yield/state/yieldRawStateAtom.ts new file mode 100644 index 0000000000..b378ead59d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/state/yieldRawStateAtom.ts @@ -0,0 +1,37 @@ +import { atom } from 'jotai/index' +import { atomWithStorage } from 'jotai/utils' + +import { atomWithPartialUpdate } from '@cowprotocol/common-utils' +import { getJotaiIsolatedStorage } from '@cowprotocol/core' +import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' + +import { DEFAULT_TRADE_DERIVED_STATE, TradeDerivedState } from 'modules/trade/types/TradeDerivedState' +import { ExtendedTradeRawState, getDefaultTradeRawState } from 'modules/trade/types/TradeRawState' + +export interface YieldDerivedState extends TradeDerivedState {} + +export interface YieldRawState extends ExtendedTradeRawState {} + +export function getDefaultYieldState(chainId: SupportedChainId | null): YieldRawState { + return { + ...getDefaultTradeRawState(chainId), + inputCurrencyAmount: null, + outputCurrencyAmount: null, + orderKind: OrderKind.SELL, + } +} + +const rawState = atomWithPartialUpdate( + atomWithStorage('yieldStateAtom:v1', getDefaultYieldState(null), getJotaiIsolatedStorage()), +) + +export const yieldRawStateAtom = atom((get) => ({ + ...get(rawState.atom), + orderKind: OrderKind.SELL, +})) + +export const updateYieldRawStateAtom = rawState.updateAtom + +export const yieldDerivedStateAtom = atom({ + ...DEFAULT_TRADE_DERIVED_STATE, +}) diff --git a/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts b/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts new file mode 100644 index 0000000000..116c884354 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts @@ -0,0 +1,19 @@ +import { atomWithStorage } from 'jotai/utils' + +import { DEFAULT_DEADLINE_FROM_NOW } from '@cowprotocol/common-const' +import { atomWithPartialUpdate } from '@cowprotocol/common-utils' +import { getJotaiIsolatedStorage } from '@cowprotocol/core' + +export interface YieldSettingsState { + readonly showRecipient: boolean + readonly deadline: number +} + +export const defaultYieldSettings: YieldSettingsState = { + showRecipient: false, + deadline: DEFAULT_DEADLINE_FROM_NOW, +} + +export const { atom: yieldSettingsAtom, updateAtom: updateYieldSettingsAtom } = atomWithPartialUpdate( + atomWithStorage('yieldSettingsAtom:v0', defaultYieldSettings, getJotaiIsolatedStorage()), +) diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/QuoteObserverUpdater/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/QuoteObserverUpdater/index.tsx new file mode 100644 index 0000000000..2390d91029 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/updaters/QuoteObserverUpdater/index.tsx @@ -0,0 +1,40 @@ +import { useEffect, useLayoutEffect } from 'react' + +import { CurrencyAmount } from '@uniswap/sdk-core' + +import { Field } from 'legacy/state/types' + +import { useReceiveAmountInfo, useDerivedTradeState } from 'modules/trade' + +import { useUpdateCurrencyAmount } from '../../hooks/useUpdateCurrencyAmount' + +export function QuoteObserverUpdater() { + const state = useDerivedTradeState() + const receiveAmountInfo = useReceiveAmountInfo() + const { beforeNetworkCosts } = receiveAmountInfo || {} + + const updateCurrencyAmount = useUpdateCurrencyAmount() + + const inputCurrency = state?.inputCurrency + const outputCurrency = state?.outputCurrency + + // Set the output amount from quote response (receiveAmountInfo is a derived state from tradeQuote state) + useLayoutEffect(() => { + if (!outputCurrency || !inputCurrency || !beforeNetworkCosts?.buyAmount) { + return + } + + updateCurrencyAmount(Field.OUTPUT, beforeNetworkCosts.buyAmount) + }, [beforeNetworkCosts, inputCurrency, outputCurrency, updateCurrencyAmount]) + + // Reset the output amount when the input amount changes + useEffect(() => { + if (!outputCurrency) { + return + } + + updateCurrencyAmount(Field.OUTPUT, CurrencyAmount.fromRawAmount(outputCurrency, 0)) + }, [state?.inputCurrencyAmount, state?.inputCurrency, updateCurrencyAmount, outputCurrency]) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx new file mode 100644 index 0000000000..e46923e292 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx @@ -0,0 +1,23 @@ +import { INITIAL_ALLOWED_SLIPPAGE_PERCENT } from '@cowprotocol/common-const' +import { percentToBps } from '@cowprotocol/common-utils' + +import { AppDataUpdater } from 'modules/appData' +import { useSetTradeQuoteParams } from 'modules/tradeQuote' + +import { QuoteObserverUpdater } from './QuoteObserverUpdater' + +import { useFillYieldDerivedState, useYieldDerivedState } from '../hooks/useYieldDerivedState' + +export function YieldUpdaters() { + const { inputCurrencyAmount } = useYieldDerivedState() + + useFillYieldDerivedState() + useSetTradeQuoteParams(inputCurrencyAmount, true) + + return ( + <> + + + + ) +} diff --git a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx index c1b37aa56f..db9d944cb9 100644 --- a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx +++ b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx @@ -48,8 +48,12 @@ export default function AdvancedOrdersPage() { params={advancedWidgetParams} mapCurrencyInfo={mapTwapCurrencyInfo} > - {/*TODO: conditionally display a widget for current advanced order type*/} - + {(tradeWarnings) => ( + <> + {/*TODO: conditionally display a widget for current advanced order type*/} + + + )} diff --git a/apps/cowswap-frontend/src/pages/Yield/index.tsx b/apps/cowswap-frontend/src/pages/Yield/index.tsx new file mode 100644 index 0000000000..0c4d78cf01 --- /dev/null +++ b/apps/cowswap-frontend/src/pages/Yield/index.tsx @@ -0,0 +1,10 @@ +import { YieldWidget, YieldUpdaters } from 'modules/yield' + +export default function YieldPage() { + return ( + <> + + + + ) +} diff --git a/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts b/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts index 3094b3c877..fcffd64457 100644 --- a/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts +++ b/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts @@ -25,6 +25,7 @@ export const ORDER_UI_TYPE_TITLES: Record = { [UiOrderType.LIMIT]: 'Limit order', [UiOrderType.TWAP]: 'TWAP order', [UiOrderType.HOOKS]: 'Hooks', + [UiOrderType.YIELD]: 'Yield', } export type UiOrderTypeParams = Pick diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/app/configurator/consts.ts index 2d229cce5e..30cf0b2f20 100644 --- a/apps/widget-configurator/src/app/configurator/consts.ts +++ b/apps/widget-configurator/src/app/configurator/consts.ts @@ -15,7 +15,7 @@ export const DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK: Record = { tradeType: 'swap, limit or advanced', sell: 'Sell token. Optionally add amount for sell orders', buy: 'Buy token. Optionally add amount for buy orders', - enabledTradeTypes: 'swap, limit and/or advanced', + enabledTradeTypes: 'swap, limit, advanced, yield', partnerFee: 'Partner fee, in Basis Points (BPS) and a receiver address', } diff --git a/libs/types/src/common.ts b/libs/types/src/common.ts index 8b40e93722..50ff3e3638 100644 --- a/libs/types/src/common.ts +++ b/libs/types/src/common.ts @@ -1,5 +1,7 @@ export type Command = () => void +export type StatefulValue = [T, (value: T) => void] + /** * UI order type that is different from existing types or classes * @@ -11,6 +13,7 @@ export enum UiOrderType { LIMIT = 'LIMIT', TWAP = 'TWAP', HOOKS = 'HOOKS', + YIELD = 'YIELD', } export type TokenInfo = { diff --git a/libs/ui/src/containers/InlineBanner/banners.tsx b/libs/ui/src/containers/InlineBanner/banners.tsx index 993d6de17a..699261d24b 100644 --- a/libs/ui/src/containers/InlineBanner/banners.tsx +++ b/libs/ui/src/containers/InlineBanner/banners.tsx @@ -3,7 +3,6 @@ import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import styled from 'styled-components/macro' -import { CowSwapSafeAppLink } from '../../containers/CowSwapSafeAppLink' import { ButtonSecondaryAlt } from '../../pure/ButtonSecondaryAlt' import { LinkStyledButton } from '../../pure/LinkStyledButton' import { TokenAmount } from '../../pure/TokenAmount' @@ -16,57 +15,6 @@ export enum BannerOrientation { Vertical = 'vertical', } -export function BundleTxApprovalBanner() { - return ( - - Token approval bundling -

- For your convenience, token approval and order placement will be bundled into a single transaction, streamlining - your experience! -

-
- ) -} - -export type BundleTxWrapBannerProps = { - nativeCurrencySymbol: string - wrappedCurrencySymbol: string -} - -export function BundleTxWrapBanner({ nativeCurrencySymbol, wrappedCurrencySymbol }: BundleTxWrapBannerProps) { - return ( - - 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! -

-
- ) -} - -// If supportsWrapping is true, nativeCurrencySymbol is required -type WrappingSupportedProps = { supportsWrapping: true; nativeCurrencySymbol: string } -// If supportsWrapping is not set or false, nativeCurrencySymbol is not required -type WrappingUnsupportedProps = { supportsWrapping?: false; nativeCurrencySymbol?: undefined } - -export type BundleTxSafeWcBannerProps = WrappingSupportedProps | WrappingUnsupportedProps - -export function BundleTxSafeWcBanner({ nativeCurrencySymbol, supportsWrapping }: BundleTxSafeWcBannerProps) { - const supportsWrappingText = supportsWrapping ? `${nativeCurrencySymbol} wrapping, ` : '' - - 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 -

-
- ) -} - export type SmallVolumeWarningBannerProps = { feePercentage: Nullish feeAmount: Nullish> diff --git a/libs/ui/src/containers/InlineBanner/index.tsx b/libs/ui/src/containers/InlineBanner/index.tsx index 0d994b1490..1e9ae9195b 100644 --- a/libs/ui/src/containers/InlineBanner/index.tsx +++ b/libs/ui/src/containers/InlineBanner/index.tsx @@ -168,6 +168,7 @@ export interface InlineBannerProps { padding?: string margin?: string width?: string + noWrapContent?: boolean onClose?: () => void } @@ -185,6 +186,7 @@ export function InlineBanner({ margin, width, onClose, + noWrapContent, }: InlineBannerProps) { const colorEnums = getColorEnums(bannerType) @@ -213,7 +215,7 @@ export function InlineBanner({ ) : !hideIcon && colorEnums.iconText ? ( {colorEnums.iconText} ) : null} - {children} + {noWrapContent ? children : {children}} {onClose && } diff --git a/libs/ui/src/pure/InfoTooltip/index.tsx b/libs/ui/src/pure/InfoTooltip/index.tsx index 4d26d37df3..03ad62fb31 100644 --- a/libs/ui/src/pure/InfoTooltip/index.tsx +++ b/libs/ui/src/pure/InfoTooltip/index.tsx @@ -32,16 +32,17 @@ const StyledTooltipContainer = styled(TooltipContainer)` export interface InfoTooltipProps { content: ReactNode + size?: number className?: string } -export function InfoTooltip({ content, className }: InfoTooltipProps) { +export function InfoTooltip({ content, className, size = 16 }: InfoTooltipProps) { const tooltipContent = {content} return ( - + ) diff --git a/libs/widget-lib/src/types.ts b/libs/widget-lib/src/types.ts index 553c3791cc..0cebd8bb1b 100644 --- a/libs/widget-lib/src/types.ts +++ b/libs/widget-lib/src/types.ts @@ -93,6 +93,7 @@ export enum TradeType { * But in the future it can be extended to support other order types. */ ADVANCED = 'advanced', + YIELD = 'yield', } /** From 8952e9f7b29ff848fa3da3f811e3e6232eb92361 Mon Sep 17 00:00:00 2001 From: Leandro Date: Fri, 18 Oct 2024 13:41:36 +0100 Subject: [PATCH 6/9] feat(explorer): update explorer graph images (#5008) * feat: update explorer graph images * fix: reduce image sizes --- apps/explorer/src/assets/img/CoW-protocol.svg | 7 ++++++- apps/explorer/src/assets/img/Trader-variant.svg | 7 ++++++- apps/explorer/src/assets/img/Trader.svg | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/explorer/src/assets/img/CoW-protocol.svg b/apps/explorer/src/assets/img/CoW-protocol.svg index 811bb20718..ef3b6cc984 100644 --- a/apps/explorer/src/assets/img/CoW-protocol.svg +++ b/apps/explorer/src/assets/img/CoW-protocol.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + diff --git a/apps/explorer/src/assets/img/Trader-variant.svg b/apps/explorer/src/assets/img/Trader-variant.svg index 4db00ab991..1a7bb1b79b 100644 --- a/apps/explorer/src/assets/img/Trader-variant.svg +++ b/apps/explorer/src/assets/img/Trader-variant.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + diff --git a/apps/explorer/src/assets/img/Trader.svg b/apps/explorer/src/assets/img/Trader.svg index 1c9a32c52f..ef1afda187 100644 --- a/apps/explorer/src/assets/img/Trader.svg +++ b/apps/explorer/src/assets/img/Trader.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + From cb589b6d99c79710350bd8a8829f2655b36b0171 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 17:51:46 +0500 Subject: [PATCH 7/9] chore: release main (#5011) --- .release-please-manifest.json | 6 +++--- apps/cowswap-frontend/CHANGELOG.md | 7 +++++++ apps/cowswap-frontend/package.json | 2 +- apps/explorer/CHANGELOG.md | 7 +++++++ apps/explorer/package.json | 2 +- libs/wallet/CHANGELOG.md | 7 +++++++ libs/wallet/package.json | 2 +- 7 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b52eed9497..4fd2f7cbd8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { - "apps/cowswap-frontend": "1.86.0", - "apps/explorer": "2.35.2", + "apps/cowswap-frontend": "1.86.1", + "apps/explorer": "2.36.0", "libs/permit-utils": "0.4.0", "libs/widget-lib": "0.16.0", "libs/widget-react": "0.11.0", @@ -17,7 +17,7 @@ "libs/tokens": "1.10.0", "libs/types": "1.2.0", "libs/ui": "1.11.0", - "libs/wallet": "1.6.0", + "libs/wallet": "1.6.1", "apps/cow-fi": "1.15.0", "libs/wallet-provider": "1.0.0", "libs/ui-utils": "1.1.0", diff --git a/apps/cowswap-frontend/CHANGELOG.md b/apps/cowswap-frontend/CHANGELOG.md index 560739bd2d..0f09d39910 100644 --- a/apps/cowswap-frontend/CHANGELOG.md +++ b/apps/cowswap-frontend/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.86.1](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.86.0...cowswap-v1.86.1) (2024-10-18) + + +### Bug Fixes + +* **widget:** ignore selected eip6963 provider when in widget ([#5009](https://github.com/cowprotocol/cowswap/issues/5009)) ([3f8446b](https://github.com/cowprotocol/cowswap/commit/3f8446b48a4f493448b262959b943756a24382d9)) + ## [1.86.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.85.0...cowswap-v1.86.0) (2024-10-18) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index a5898d6e9b..0b9af36fdc 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cowswap", - "version": "1.86.0", + "version": "1.86.1", "description": "CoW Swap", "main": "index.js", "author": "", diff --git a/apps/explorer/CHANGELOG.md b/apps/explorer/CHANGELOG.md index 38a0d41e0f..b35339d854 100644 --- a/apps/explorer/CHANGELOG.md +++ b/apps/explorer/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.36.0](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.2...explorer-v2.36.0) (2024-10-18) + + +### Features + +* **explorer:** update explorer graph images ([#5008](https://github.com/cowprotocol/cowswap/issues/5008)) ([8952e9f](https://github.com/cowprotocol/cowswap/commit/8952e9f7b29ff848fa3da3f811e3e6232eb92361)) + ## [2.35.2](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.1...explorer-v2.35.2) (2024-10-18) diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 541ed15881..23375d4751 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/explorer", - "version": "2.35.2", + "version": "2.36.0", "description": "CoW Swap Explorer", "main": "src/main.tsx", "author": "", diff --git a/libs/wallet/CHANGELOG.md b/libs/wallet/CHANGELOG.md index d902625ba5..19aa9378f6 100644 --- a/libs/wallet/CHANGELOG.md +++ b/libs/wallet/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.6.1](https://github.com/cowprotocol/cowswap/compare/wallet-v1.6.0...wallet-v1.6.1) (2024-10-18) + + +### Bug Fixes + +* **widget:** ignore selected eip6963 provider when in widget ([#5009](https://github.com/cowprotocol/cowswap/issues/5009)) ([3f8446b](https://github.com/cowprotocol/cowswap/commit/3f8446b48a4f493448b262959b943756a24382d9)) + ## [1.6.0](https://github.com/cowprotocol/cowswap/compare/wallet-v1.5.2...wallet-v1.6.0) (2024-09-30) diff --git a/libs/wallet/package.json b/libs/wallet/package.json index c06ecfef10..fc6205c015 100644 --- a/libs/wallet/package.json +++ b/libs/wallet/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/wallet", - "version": "1.6.0", + "version": "1.6.1", "main": "./index.js", "types": "./index.d.ts", "exports": { From 1abd82527dc1f96d6897533d750dcc6f2a51e7a0 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 18 Oct 2024 17:46:25 +0100 Subject: [PATCH 8/9] fix: fix bad merge --- .../swap/containers/ConfirmSwapModalSetup/index.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index c7bedcf835..f0b391f752 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -20,9 +20,8 @@ import { useTradeConfirmActions, } from 'modules/trade' import { TradeBasicConfirmDetails } from 'modules/trade/containers/TradeBasicConfirmDetails' -import { useIsSmartSlippageApplied } from 'modules/tradeSlippage' -import { HighFeeWarning } from 'modules/tradeWidgetAddons' -import { NetworkCostsTooltipSuffix, RowDeadline } from 'modules/tradeWidgetAddons' +import { useIsSmartSlippageApplied, useSmartTradeSlippage } from 'modules/tradeSlippage' +import { HighFeeWarning, NetworkCostsTooltipSuffix, RowDeadline } from 'modules/tradeWidgetAddons' import { CurrencyPreviewInfo } from 'common/pure/CurrencyAmountPreview' import { NetworkCostsSuffix } from 'common/pure/NetworkCostsSuffix' @@ -31,7 +30,6 @@ import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/ut import useNativeCurrency from 'lib/hooks/useNativeCurrency' import { useSwapConfirmButtonText } from '../../hooks/useSwapConfirmButtonText' -import { useSmartSwapSlippage } from '../../hooks/useSwapSlippage' import { useSwapState } from '../../hooks/useSwapState' const CONFIRM_TITLE = 'Swap' @@ -79,7 +77,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const buttonText = useSwapConfirmButtonText(slippageAdjustedSellAmount) const isSmartSlippageApplied = useIsSmartSlippageApplied() - const smartSlippage = useSmartSwapSlippage() + const smartSlippage = useSmartTradeSlippage() const labelsAndTooltips = useMemo( () => ({ From 9308fc1e35ce5ecfdc69c76974136182352eeca0 Mon Sep 17 00:00:00 2001 From: Leandro Date: Mon, 21 Oct 2024 16:15:51 +0100 Subject: [PATCH 9/9] fix(smart-slippage): replace volatity with trade size on tooltips (#5012) --- .../containers/HighSuggestedSlippageWarning/index.tsx | 2 +- .../containers/TransactionSettings/index.tsx | 4 ++-- .../tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx b/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx index 225e0b99f9..ba9afaaa33 100644 --- a/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx @@ -32,7 +32,7 @@ export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarning Slippage adjusted to {`${slippageBps / 100}`}% to ensure quick execution ) diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx index 4ec57aa276..eb3d25cfb9 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx @@ -279,8 +279,8 @@ export function TransactionSettings({ deadlineState }: TransactionSettingsProps) - CoW Swap has dynamically selected this slippage amount to account for current gas prices and - volatility. Changes may result in slower execution. + CoW Swap has dynamically selected this slippage amount to account for current gas prices and trade + size. Changes may result in slower execution. } /> diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx index 76ccec401d..741bc17006 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components/macro' import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' import { settingsTabStateAtom } from '../../../state/settingsTabState' -import { StyledRowBetween, TextWrapper, StyledInfoIcon, TransactionText, RowStyleProps } from '../styled' +import { RowStyleProps, StyledInfoIcon, StyledRowBetween, TextWrapper, TransactionText } from '../styled' const DefaultSlippage = styled.span` display: inline-flex; @@ -30,7 +30,7 @@ const DefaultSlippage = styled.span` ` const SUGGESTED_SLIPPAGE_TOOLTIP = - 'This is the recommended slippage tolerance based on current gas prices & volatility. A lower amount may result in slower execution.' + 'This is the recommended slippage tolerance based on current gas prices & trade size. A lower amount may result in slower execution.' export interface RowSlippageContentProps { chainId: SupportedChainId